From 8f52e73bb9744586d07ec930b51d0c2062c913a6 Mon Sep 17 00:00:00 2001 From: Eremel Date: Sun, 11 Jan 2026 23:01:42 +0000 Subject: [PATCH 01/32] Initial commit --- lovely/weights.toml | 72 ++++++++++++++++ src/core.lua | 1 + src/utils.lua | 4 +- src/utils/weights.lua | 188 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 263 insertions(+), 2 deletions(-) create mode 100644 lovely/weights.toml create mode 100644 src/utils/weights.lua diff --git a/lovely/weights.toml b/lovely/weights.toml new file mode 100644 index 000000000..51eee6623 --- /dev/null +++ b/lovely/weights.toml @@ -0,0 +1,72 @@ +[manifest] +version = "1.0.0" +dump_lua = true +priority = -10 + + +# Escape get_new_boss early for SMODS.poll_object +[[patches]] +[patches.pattern] +target = 'functions/common_events.lua' +match_indent = true +position = 'before' +pattern = ''' +local _, boss = pseudorandom_element(eligible_bosses, pseudoseed('boss')) +''' +payload = ''' +if early_return then + local boss_keys = {} + for k, _ in pairs(eligible_bosses) do table.insert(boss_keys, k) end + return boss_keys +end + +''' + +# Add early_return argument to get_new_boss +[[patches]] +[patches.pattern] +target = 'functions/common_events.lua' +match_indent = true +position = 'at' +pattern = ''' +function get_new_boss() +''' +payload = ''' +function get_new_boss(early_return) +''' + +# Adjust vanilla blinds max ante property +[[patches]] +[patches.pattern] +target = 'game.lua' +match_indent = true +position = 'before' +pattern = ''' +self.b_undiscovered = {name = 'Undiscovered', debuff_text = 'Defeat this blind to discover', pos = {x=0,y=30}} +''' +payload = ''' +for key, blind in pairs(self.P_BLINDS) do + if blind.boss and blind.boss.max then + blind.boss.max = nil + if blind.boss.showdown then + blind.boss.min = nil + end + end + if key == 'bl_small' then blind.small = {min = 1} end + if key == 'bl_big' then blind.big = {min = 1} end +end +''' + +# Add custom small/big blinds to `G.GAME.bosses_used` +[[patches]] +[patches.pattern] +target = 'game.lua' +match_indent = true +position = 'after' +pattern = ''' +if v.boss then bosses_used[k] = 0 end +''' +payload = ''' +if v.small or v.big then bosses_used[k] = 0 end +''' + diff --git a/src/core.lua b/src/core.lua index 8fe2f0b0f..739c10431 100644 --- a/src/core.lua +++ b/src/core.lua @@ -73,6 +73,7 @@ for _, path in ipairs { "src/logging.lua", "src/compat_0_9_8.lua", "src/loader.lua", + "src/utils/weights.lua" } do assert(load(NFS.read(SMODS.path..path), ('=[SMODS _ "%s"]'):format(path)))() end diff --git a/src/utils.lua b/src/utils.lua index 0ff3f3ad2..7c85340bf 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -709,7 +709,7 @@ end function SMODS.poll_edition(args) args = args or {} - return poll_edition(args.key or 'editiongeneric', args.mod, args.no_negative, args.guaranteed, args.options) + return poll_edition(args.key or 'edition_generic', args.mod, args.no_negative, args.guaranteed, args.options) end function SMODS.poll_seal(args) @@ -728,7 +728,7 @@ function SMODS.poll_seal(args) local seal_option = {} if type(v) == 'string' then assert(G.P_SEALS[v], ("Could not find seal \"%s\"."):format(v)) - seal_option = { key = v, weight = G.P_SEALS[v].weight or 5 } -- default weight set to 5 to replicate base game weighting + seal_option = { key = v, weight = G.P_SEALS[v].weight or 10 } -- default weight set to 5 to replicate base game weighting elseif type(v) == 'table' then assert(G.P_SEALS[v.key], ("Could not find seal \"%s\"."):format(v.key)) seal_option = { key = v.key, weight = v.weight } diff --git a/src/utils/weights.lua b/src/utils/weights.lua new file mode 100644 index 000000000..ef3f2503a --- /dev/null +++ b/src/utils/weights.lua @@ -0,0 +1,188 @@ +-- TODO: labels are union or interset +-- TODO: filter function accepted + +-- Returns a `key` of the polled object type +---@param args table|{type: string?, labels: table[string]?, pool: table[string]?, seed: string?, chance: number?, guaranteed: boolean?} +function SMODS.poll_object(args) + assert(args, "SMODS.poll_object called with args."..SMODS.log_crash_info(debug.getinfo(2))) + assert((args.type or (args.labels and type(args.labels) == 'table') or (args.pool and type(args.pool) == 'table')), "SMODS.poll_object called without a pool source." .. SMODS.log_crash_info(debug.getinfo(2))) + + -- Prepare pool + local pool = args.pool or {} + local types = args.labels or {args.type} + local total_weight = 0 + local modded_weight = 0 + local chance = args.guaranteed and 1 or args.chance or SMODS.base_rate_percentage[args.type] or 1 + local poll_key = args.guaranteed and 1 or pseudorandom(pseudoseed(args.seed or SMODS.get_poll_key(args.type))) + + for _, label in ipairs(types) do + local temp_pool = {} + for i=1, #(args.rarities or {true}) do + local _p = label == 'Blind' and get_new_boss(true) or get_current_pool(label, args.rarities and args.rarities[i]) + if label == 'Edition' then + local _options = {} + for _, edition in ipairs(_p) do + if G.P_CENTERS[edition] and G.P_CENTERS[edition].vanilla then + table.insert(_options, 1, edition) + elseif G.P_CENTERS[edition] then + table.insert(_options, edition) + end + end + _p = _options + end + temp_pool = SMODS.merge_lists({temp_pool, _p}) + end + for _, v in ipairs(temp_pool) do + if G[SMODS.game_table_from_type[label] or 'P_CENTERS'][v] then table.insert(pool, {key = v, type = label}) end + end + end + + -- Check pool has valid options + assert(#pool > 0, "SMODS.poll_object called with an empty pool."..SMODS.log_crash_info(debug.getinfo(2))) + + + local final_pool = {} + for _, key in ipairs(pool) do + local weight_table = {} + + local w, m_w = SMODS.get_weight_of_object(G[SMODS.game_table_from_type[key.type] or 'P_CENTERS'][key.key], key.weight) + modded_weight = modded_weight + m_w + weight_table = {key = key.key, weight = w, mod_weight = modded_weight} + + total_weight = total_weight + weight_table.weight + table.insert(final_pool, weight_table) + if args.print then print(string.format("Key: %s, Weight: %s, Modded Weight: %s", weight_table.key, weight_table.weight, weight_table.mod_weight)) end + end + + + if args.print then print('Total Weight: '..total_weight) end + if args.print then print('Modded Weight:'..modded_weight) end + if args.print then print('Base Chance: '..chance) end + + -- Adjust chance based on modified weightings + chance = chance * (modded_weight/total_weight) + if args.print then print('Mod Chance: '..chance) end + if args.print then print('Poll Key:'..poll_key) end + + if poll_key < (1 - chance) then + if args.print then print('Poll failed') end + return + end + + if not SMODS.no_repoll[args.type] then + poll_key = pseudorandom(pseudoseed(SMODS.get_poll_key(args.type, 'type'))) + chance = 1 + end + if args.print then print('Poll key: '..poll_key) end + + -- Find weight + local poll_weight = modded_weight - (poll_key - (1 - chance))/chance * modded_weight + if args.print then print('Looking for item: '..poll_weight) end + local low = 1 + local high = #final_pool + if poll_weight < final_pool[1].mod_weight then return final_pool[1].key end + + local ind = SMODS.select_by_weight(final_pool, poll_weight, low, high) + -- print('Index: '..ind) + -- print(final_pool[ind].key) + return final_pool[ind].key +end + +-- Returns the `weight` and `modified_weight` or a given object +---@param args table|{key: string, no_mod: boolean?} +function SMODS.get_weight_of_object(obj, opt_weight) + local w = opt_weight or obj.weight or 10 + local m = not opt_weight and obj.get_weight and obj:get_weight(w) or w + + return w, m +end + +function SMODS.select_by_weight(pool, poll, low, high, depth) + if high - low <= 1 then return high end + local check = math.floor((low + high)/2) + if poll < pool[check].mod_weight then + high = check + else + low = check + end + return SMODS.select_by_weight(pool, poll, low, high, (depth or 0) + 1) +end + +SMODS.base_rate_percentage = { + Enhanced = 0.40, + Seal = 0.02, + Edition = 0.04 +} + +SMODS.no_repoll = { + Edition = true +} + +SMODS.game_table_from_type = { + Seal = 'P_SEALS', + Tag = 'P_TAGS', + Blind = 'P_BLINDS', + Card = 'P_CARDS', + Stake = 'P_STAKES' +} + +SMODS.poll_keys = { + Edition = {str = 'edition_generic', block_infill = true}, + Seal = {str = 'stdseal', ante = true}, + Enhanced = {str = 'std_enhance', ante = true} +} + +function SMODS.get_poll_key(type, infill) + local t = SMODS.poll_keys[type] or {str = 'std_smods_poll', ante = true} + return t.str .. (t.block_infill and "" or infill or "") .. (t.ante and G.GAME.round_resets.ante or "") +end + +function SMODS.create_blind_pool(type, skip_cull) + local eligible_bosses = {} + for k, v in pairs(G.P_BLINDS) do + local res, options = SMODS.add_to_pool(v) + options = options or {} + if not v[type] then + elseif options.ignore_showdown_check then + eligible_bosses[k] = res and true or nil + elseif type == 'boss' then + if + ((SMODS.is_showdown_ante()) == (v.boss.showdown or false)) and ((v[type].min or G.GAME.round_resets.ante) <= math.max(1, G.GAME.round_resets.ante)) and ((v[type].max or G.GAME.round_resets.ante) >= G.GAME.round_resets.ante) + then + eligible_bosses[k] = res and true or nil + end + else + if (v[type].min or G.GAME.round_resets.ante) <= math.max(1, G.GAME.round_resets.ante) and (v[type].max or G.GAME.round_resets.ante) >= G.GAME.round_resets.ante then + eligible_bosses[k] = res and true or nil + end + end + end + for k, v in pairs(G.GAME.banned_keys) do + if eligible_bosses[k] then eligible_bosses[k] = nil end + end + + if skip_cull then return eligible_bosses end + + local min_use = 100 + for k, v in pairs(G.GAME.bosses_used) do + if eligible_bosses[k] then + eligible_bosses[k] = v + if eligible_bosses[k] <= min_use then + min_use = eligible_bosses[k] + end + end + end + for k, v in pairs(eligible_bosses) do + if eligible_bosses[k] then + if eligible_bosses[k] > min_use then + eligible_bosses[k] = nil + end + end + end + + return eligible_bosses +end + +function SMODS.is_showdown_ante() + return G.GAME.round_resets.ante%G.GAME.win_ante == 0 and G.GAME.round_resets.ante > 0 +end \ No newline at end of file From 410d7f4c594724ac5e29ab584f086941ebe4b918 Mon Sep 17 00:00:00 2001 From: Eremel Date: Sun, 8 Feb 2026 14:39:50 +0000 Subject: [PATCH 02/32] Fix loading with preflight --- src/core.lua | 1 - src/utils/weights.lua | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/core.lua b/src/core.lua index 31c4c50fa..9102d8599 100644 --- a/src/core.lua +++ b/src/core.lua @@ -6,7 +6,6 @@ for _, path in ipairs { "src/overrides.lua", "src/game_object.lua", "src/compat_0_9_8.lua", - "src/loader.lua", "src/utils/weights.lua" } do assert(load(SMODS.NFS.read(SMODS.path..path), ('=[SMODS _ "%s"]'):format(path)))() diff --git a/src/utils/weights.lua b/src/utils/weights.lua index ef3f2503a..f773f4c7b 100644 --- a/src/utils/weights.lua +++ b/src/utils/weights.lua @@ -4,7 +4,7 @@ -- Returns a `key` of the polled object type ---@param args table|{type: string?, labels: table[string]?, pool: table[string]?, seed: string?, chance: number?, guaranteed: boolean?} function SMODS.poll_object(args) - assert(args, "SMODS.poll_object called with args."..SMODS.log_crash_info(debug.getinfo(2))) + assert(args, "SMODS.poll_object called with no args."..SMODS.log_crash_info(debug.getinfo(2))) assert((args.type or (args.labels and type(args.labels) == 'table') or (args.pool and type(args.pool) == 'table')), "SMODS.poll_object called without a pool source." .. SMODS.log_crash_info(debug.getinfo(2))) -- Prepare pool From 282be5aea95bbf00dfc5c4edc105f6632cff573a Mon Sep 17 00:00:00 2001 From: Eremel Date: Sun, 8 Feb 2026 20:33:13 +0000 Subject: [PATCH 03/32] Add support for `pool` to be defined as a table of keys --- src/utils/weights.lua | 46 ++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/utils/weights.lua b/src/utils/weights.lua index f773f4c7b..1e9d7df40 100644 --- a/src/utils/weights.lua +++ b/src/utils/weights.lua @@ -13,41 +13,43 @@ function SMODS.poll_object(args) local total_weight = 0 local modded_weight = 0 local chance = args.guaranteed and 1 or args.chance or SMODS.base_rate_percentage[args.type] or 1 - local poll_key = args.guaranteed and 1 or pseudorandom(pseudoseed(args.seed or SMODS.get_poll_key(args.type))) - - for _, label in ipairs(types) do - local temp_pool = {} - for i=1, #(args.rarities or {true}) do - local _p = label == 'Blind' and get_new_boss(true) or get_current_pool(label, args.rarities and args.rarities[i]) - if label == 'Edition' then - local _options = {} - for _, edition in ipairs(_p) do - if G.P_CENTERS[edition] and G.P_CENTERS[edition].vanilla then - table.insert(_options, 1, edition) - elseif G.P_CENTERS[edition] then - table.insert(_options, edition) + local poll_key = pseudorandom(pseudoseed(args.seed or SMODS.get_poll_key(args.type))) + if not args.pool then + for _, label in ipairs(types) do + local temp_pool = {} + for i=1, #(args.rarities or {true}) do + local _p = label == 'Blind' and get_new_boss(true) or get_current_pool(label, args.rarities and args.rarities[i]) + if label == 'Edition' then + local _options = {} + for _, edition in ipairs(_p) do + if G.P_CENTERS[edition] and G.P_CENTERS[edition].vanilla then + table.insert(_options, 1, edition) + elseif G.P_CENTERS[edition] then + table.insert(_options, edition) + end end + _p = _options end - _p = _options + temp_pool = SMODS.merge_lists({temp_pool, _p}) + end + for _, v in ipairs(temp_pool) do + if G[SMODS.game_table_from_type[label] or 'P_CENTERS'][v] then table.insert(pool, {key = v, type = label}) end end - temp_pool = SMODS.merge_lists({temp_pool, _p}) - end - for _, v in ipairs(temp_pool) do - if G[SMODS.game_table_from_type[label] or 'P_CENTERS'][v] then table.insert(pool, {key = v, type = label}) end end end - + + if args.filter then pool = args.filter(pool) end + -- Check pool has valid options assert(#pool > 0, "SMODS.poll_object called with an empty pool."..SMODS.log_crash_info(debug.getinfo(2))) - local final_pool = {} for _, key in ipairs(pool) do local weight_table = {} - local w, m_w = SMODS.get_weight_of_object(G[SMODS.game_table_from_type[key.type] or 'P_CENTERS'][key.key], key.weight) + local w, m_w = SMODS.get_weight_of_object(G[SMODS.game_table_from_type[key.type] or 'P_CENTERS'][key.key or key], key.weight) modded_weight = modded_weight + m_w - weight_table = {key = key.key, weight = w, mod_weight = modded_weight} + weight_table = {key = key.key or key, weight = w, mod_weight = modded_weight} total_weight = total_weight + weight_table.weight table.insert(final_pool, weight_table) From c6a9621a5fd1f893795c51b54a2a16c5e43f1809 Mon Sep 17 00:00:00 2001 From: Eremel Date: Fri, 13 Feb 2026 21:47:33 +0000 Subject: [PATCH 04/32] temp commit --- lovely/weights.toml | 13 ++++ src/overrides.lua | 144 ++++++++++++++++++++++-------------------- src/utils/weights.lua | 18 ++++-- 3 files changed, 101 insertions(+), 74 deletions(-) diff --git a/lovely/weights.toml b/lovely/weights.toml index 51eee6623..e937aa3d3 100644 --- a/lovely/weights.toml +++ b/lovely/weights.toml @@ -70,3 +70,16 @@ payload = ''' if v.small or v.big then bosses_used[k] = 0 end ''' +# comment +[[patches]] +[patches.pattern] +target = 'functions/common_events.lua' +match_indent = true +position = 'before' +pattern = ''' +center = pseudorandom_element(_pool, pseudoseed(_pool_key..'_resample'..it)) +''' +payload = ''' +print(_pool_key..'_resample'..it) +print(_pool) +''' diff --git a/src/overrides.lua b/src/overrides.lua index 296293b45..6d78dc591 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -2057,6 +2057,7 @@ function Card:set_edition(edition, immediate, silent, delay) SMODS.enh_cache:write(self, nil) if self.edition then + print('hitting here') self.ability.card_limit = self.ability.card_limit - (self.edition.card_limit or 0) self.ability.extra_slots_used = self.ability.extra_slots_used - (self.edition.extra_slots_used or 0) end @@ -2196,6 +2197,7 @@ function Card:set_edition(edition, immediate, silent, delay) end self.ability.card_limit = self.ability.card_limit + (self.edition.card_limit or 0) + print('hi there') self.ability.extra_slots_used = self.ability.extra_slots_used + (self.edition.extra_slots_used or 0) @@ -2213,73 +2215,81 @@ end -- _options = list of keys of editions to include in the poll -- OR list of tables { name = key, weight = number } function poll_edition(_key, _mod, _no_neg, _guaranteed, _options) - local _modifier = 1 - local edition_poll = pseudorandom(pseudoseed(_key or 'edition_generic')) -- Generate the poll value - local available_editions = {} -- Table containing a list of editions and their weights - - if not _options then - if _key == "wheel_of_fortune" or _key == "aura" then -- set base game edition polling - _options = { 'e_negative', 'e_polychrome', 'e_holo', 'e_foil' } - else - local unordered_options = get_current_pool("Edition", nil, nil, _key or 'edition_generic') - _options = {} - for _, edition in ipairs(unordered_options) do -- Flip the order of vanilla editions - if G.P_CENTERS[edition] and G.P_CENTERS[edition].vanilla then - table.insert(_options, 1, edition) - else - table.insert(_options, edition) - end - end - end - end - for _, v in ipairs(_options) do - local edition_option = {} - if type(v) == 'string' then - if v ~= 'UNAVAILABLE' then - assert(string.sub(v, 1, 2) == 'e_', ("Edition \"%s\" is missing \"e_\" prefix."):format(v)) - edition_option = { name = v, weight = G.P_CENTERS[v].weight } - table.insert(available_editions, edition_option) - end - elseif type(v) == 'table' then - assert(string.sub(v.name, 1, 2) == 'e_', ("Edition \"%s\" is missing \"e_\" prefix."):format(v.name)) - edition_option = { name = v.name, weight = v.weight } - table.insert(available_editions, edition_option) - end - end - - -- Calculate total weight of editions - local total_weight = 0 - for _, v in ipairs(available_editions) do - total_weight = total_weight + (v.weight) -- total all the weights of the polled editions - end - -- sendDebugMessage("Edition weights: "..total_weight, "EditionAPI") - -- If not guaranteed, calculate the base card rate to maintain base 4% chance of editions - if not _guaranteed then - _modifier = _mod or 1 - total_weight = total_weight + (total_weight / 4 * 96) -- Find total weight with base_card_rate as 96% - for _, v in ipairs(available_editions) do - v.weight = G.P_CENTERS[v.name]:get_weight() -- Apply game modifiers where appropriate (defined in edition declaration) - end - end - -- sendDebugMessage("Total weight: "..total_weight, "EditionAPI") - -- sendDebugMessage("Editions: "..#available_editions, "EditionAPI") - -- sendDebugMessage("Poll: "..edition_poll, "EditionAPI") - - -- Calculate whether edition is selected - local weight_i = 0 - for _, v in ipairs(available_editions) do - weight_i = weight_i + v.weight * _modifier - -- sendDebugMessage(v.name.." weight is "..v.weight*_modifier, "EditionAPI") - -- sendDebugMessage("Checking for "..v.name.." at "..(1 - (weight_i)/total_weight), "EditionAPI") - if edition_poll > 1 - (weight_i) / total_weight then - if not (v.name == 'e_negative' and _no_neg) then -- skip return if negative is selected and _no_neg is true - -- sendDebugMessage("Matched edition: "..v.name, "EditionAPI") - return v.name - end - end - end - - return nil + if not _options and (_key == "wheel_of_fortune" or _key == "aura") then -- set base game edition polling + _options = { 'e_negative', 'e_polychrome', 'e_holo', 'e_foil' } + end + + return SMODS.poll_object({type = 'Edition', seed = _key, guaranteed = _guaranteed, pool = _options, no_negative = _no_neg, mod = _mod}) + + -- REMOVED FOR NEW FUNCTION SMODS.poll_object + + -- local _modifier = 1 + -- local edition_poll = pseudorandom(pseudoseed(_key or 'edition_generic')) -- Generate the poll value + -- local available_editions = {} -- Table containing a list of editions and their weights + + -- if not _options then + -- if _key == "wheel_of_fortune" or _key == "aura" then -- set base game edition polling + -- _options = { 'e_negative', 'e_polychrome', 'e_holo', 'e_foil' } + -- else + -- local unordered_options = get_current_pool("Edition", nil, nil, _key or 'edition_generic') + -- _options = {} + -- for _, edition in ipairs(unordered_options) do -- Flip the order of vanilla editions + -- if G.P_CENTERS[edition] and G.P_CENTERS[edition].vanilla then + -- table.insert(_options, 1, edition) + -- else + -- table.insert(_options, edition) + -- end + -- end + -- end + -- end + -- for _, v in ipairs(_options) do + -- local edition_option = {} + -- if type(v) == 'string' then + -- if v ~= 'UNAVAILABLE' then + -- assert(string.sub(v, 1, 2) == 'e_', ("Edition \"%s\" is missing \"e_\" prefix."):format(v)) + -- edition_option = { name = v, weight = G.P_CENTERS[v].weight } + -- table.insert(available_editions, edition_option) + -- end + -- elseif type(v) == 'table' then + -- assert(string.sub(v.name, 1, 2) == 'e_', ("Edition \"%s\" is missing \"e_\" prefix."):format(v.name)) + -- edition_option = { name = v.name, weight = v.weight } + -- table.insert(available_editions, edition_option) + -- end + -- end + + -- -- Calculate total weight of editions + -- local total_weight = 0 + -- for _, v in ipairs(available_editions) do + -- total_weight = total_weight + (v.weight) -- total all the weights of the polled editions + -- end + -- -- sendDebugMessage("Edition weights: "..total_weight, "EditionAPI") + -- -- If not guaranteed, calculate the base card rate to maintain base 4% chance of editions + -- if not _guaranteed then + -- _modifier = _mod or 1 + -- total_weight = total_weight + (total_weight / 4 * 96) -- Find total weight with base_card_rate as 96% + -- for _, v in ipairs(available_editions) do + -- v.weight = G.P_CENTERS[v.name]:get_weight() -- Apply game modifiers where appropriate (defined in edition declaration) + -- end + -- end + -- -- sendDebugMessage("Total weight: "..total_weight, "EditionAPI") + -- -- sendDebugMessage("Editions: "..#available_editions, "EditionAPI") + -- -- sendDebugMessage("Poll: "..edition_poll, "EditionAPI") + + -- -- Calculate whether edition is selected + -- local weight_i = 0 + -- for _, v in ipairs(available_editions) do + -- weight_i = weight_i + v.weight * _modifier + -- -- sendDebugMessage(v.name.." weight is "..v.weight*_modifier, "EditionAPI") + -- -- sendDebugMessage("Checking for "..v.name.." at "..(1 - (weight_i)/total_weight), "EditionAPI") + -- if edition_poll > 1 - (weight_i) / total_weight then + -- if not (v.name == 'e_negative' and _no_neg) then -- skip return if negative is selected and _no_neg is true + -- -- sendDebugMessage("Matched edition: "..v.name, "EditionAPI") + -- return v.name + -- end + -- end + -- end + + -- return nil end -- local cge = Card.get_edition diff --git a/src/utils/weights.lua b/src/utils/weights.lua index 1e9d7df40..6fa391d37 100644 --- a/src/utils/weights.lua +++ b/src/utils/weights.lua @@ -12,7 +12,7 @@ function SMODS.poll_object(args) local types = args.labels or {args.type} local total_weight = 0 local modded_weight = 0 - local chance = args.guaranteed and 1 or args.chance or SMODS.base_rate_percentage[args.type] or 1 + local chance = (args.guaranteed and 1 or args.chance or SMODS.base_rate_percentage[args.type] or 1) * (args.mod or 1) local poll_key = pseudorandom(pseudoseed(args.seed or SMODS.get_poll_key(args.type))) if not args.pool then for _, label in ipairs(types) do @@ -72,7 +72,8 @@ function SMODS.poll_object(args) end if not SMODS.no_repoll[args.type] then - poll_key = pseudorandom(pseudoseed(SMODS.get_poll_key(args.type, 'type'))) + poll_key = pseudorandom(pseudoseed(args.type_key or SMODS.get_poll_key(args.type, args.append or 'type'))) + if args.print then print('Poll key string:', args.type_key or SMODS.get_poll_key(args.type, args.append or 'type')) end chance = 1 end if args.print then print('Poll key: '..poll_key) end @@ -82,17 +83,20 @@ function SMODS.poll_object(args) if args.print then print('Looking for item: '..poll_weight) end local low = 1 local high = #final_pool - if poll_weight < final_pool[1].mod_weight then return final_pool[1].key end - - local ind = SMODS.select_by_weight(final_pool, poll_weight, low, high) + local ind = 1 + if poll_weight > final_pool[1].mod_weight then ind = SMODS.select_by_weight(final_pool, poll_weight, low, high) end -- print('Index: '..ind) -- print(final_pool[ind].key) + + if args.no_negative and final_pool[ind].key == 'e_negative' then return 'e_polychrome' end + return final_pool[ind].key end -- Returns the `weight` and `modified_weight` or a given object ---@param args table|{key: string, no_mod: boolean?} function SMODS.get_weight_of_object(obj, opt_weight) + if not obj then return 10, 10 end local w = opt_weight or obj.weight or 10 local m = not opt_weight and obj.get_weight and obj:get_weight(w) or w @@ -117,7 +121,7 @@ SMODS.base_rate_percentage = { } SMODS.no_repoll = { - Edition = true + Edition = true, } SMODS.game_table_from_type = { @@ -131,7 +135,7 @@ SMODS.game_table_from_type = { SMODS.poll_keys = { Edition = {str = 'edition_generic', block_infill = true}, Seal = {str = 'stdseal', ante = true}, - Enhanced = {str = 'std_enhance', ante = true} + Enhanced = {str = 'Enhanced', ante = true} } function SMODS.get_poll_key(type, infill) From 379ca25a8e2fe0397637b60cb82ee375a4a10afc Mon Sep 17 00:00:00 2001 From: Eremel Date: Sat, 21 Mar 2026 23:20:59 +0000 Subject: [PATCH 05/32] Update --- lovely/card_limit.toml | 2 +- src/card_draw.lua | 2 +- src/overrides.lua | 2 +- src/utils.lua | 20 ++-- src/utils/weights.lua | 234 +++++++++++++++++++++++++++++++++-------- 5 files changed, 201 insertions(+), 59 deletions(-) diff --git a/lovely/card_limit.toml b/lovely/card_limit.toml index d9de42e5d..2386fcb6c 100644 --- a/lovely/card_limit.toml +++ b/lovely/card_limit.toml @@ -292,7 +292,7 @@ position = "at" payload = """local hand_space = e local cards_to_draw = {} local space_taken = 0 -local limit = G.hand.config.card_limit - #G.hand.cards - (SMODS.cards_to_draw or 0) +local limit = (G.hand.config.card_limit - #G.hand.cards - (SMODS.cards_to_draw or 0)) local flags = SMODS.calculate_context({drawing_cards = true, amount = limit}) limit = flags.cards_to_draw or flags.modify or limit local unfixed = not G.hand.config.fixed_limit diff --git a/src/card_draw.lua b/src/card_draw.lua index 8cd1b2fa7..3fa231547 100644 --- a/src/card_draw.lua +++ b/src/card_draw.lua @@ -49,7 +49,7 @@ function SMODS.clean_up_canvas_text(t) end function SMODS.clean_up_children(t) - local ignore = {center = true, shadow = true, back = true, h_popup = true} + local ignore = {center = true, shadow = true, back = true, h_popup = true, front = true} for k, v in pairs(t) do if not ignore[k] then if type(v) == 'table' and v.remove then v:remove() end diff --git a/src/overrides.lua b/src/overrides.lua index 9025a675e..e7b792495 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -2204,7 +2204,6 @@ function Card:set_edition(edition, immediate, silent, delay) end self.ability.card_limit = self.ability.card_limit + (self.edition.card_limit or 0) - print('hi there') self.ability.extra_slots_used = self.ability.extra_slots_used + (self.edition.extra_slots_used or 0) @@ -2682,6 +2681,7 @@ function Card:set_ability(center, initial, delay_sprites) if not initial and (G.STATE ~= G.STATES.SMODS_BOOSTER_OPENED and G.STATE ~= G.STATES.SHOP and not G.SETTINGS.paused or G.TAROT_INTERRUPT) then SMODS.calculate_context({setting_ability = true, old = old_center.key, new = self.config.center_key, other_card = self, unchanged = old_center.key == self.config.center.key}) end + self.front_hidden = self:should_hide_front() end local add_tag_ref = add_tag diff --git a/src/utils.lua b/src/utils.lua index 9d102eb62..f574f1d0e 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -2127,18 +2127,18 @@ function SMODS.get_card_areas(_type, _context) end if _type == 'individual' then local t = { - { object = G.GAME.selected_back, scored_card = G.deck.cards[1] or G.deck }, + { object = G.GAME.selected_back, scored_card = G.deck and G.deck.cards[1] or G.deck }, } if G.GAME.blind and G.GAME.blind.children and G.GAME.blind.children.animatedSprite then t[#t + 1] = { object = G.GAME.blind, scored_card = G.GAME.blind.children.animatedSprite } end - if G.GAME.challenge then t[#t + 1] = { object = SMODS.Challenges[G.GAME.challenge], scored_card = G.deck.cards[1] or G.deck } end + if G.GAME.challenge then t[#t + 1] = { object = SMODS.Challenges[G.GAME.challenge], scored_card = G.deck and G.deck.cards[1] or G.deck } end for _, stake in ipairs(SMODS.get_stake_scoring_targets()) do - t[#t + 1] = { object = stake, scored_card = G.deck.cards[1] or G.deck } + t[#t + 1] = { object = stake, scored_card = G.deck and G.deck.cards[1] or G.deck } end for _, mod in ipairs(SMODS.get_mods_scoring_targets()) do - t[#t + 1] = { object = mod, scored_card = G.deck.cards[1] or G.deck } + t[#t + 1] = { object = mod, scored_card = G.deck and G.deck.cards[1] or G.deck } end -- TARGET: add your own individual scoring targets return t @@ -2330,12 +2330,8 @@ function SMODS.get_next_vouchers(vouchers) vouchers = vouchers or {spawn = {}} local _pool, _pool_key = get_current_pool('Voucher') for i=#vouchers+1, math.min(SMODS.size_of_pool(_pool), G.GAME.starting_params.vouchers_in_shop + (G.GAME.modifiers.extra_vouchers or 0)) do - local center = pseudorandom_element(_pool, pseudoseed(_pool_key)) - local it = 1 - while center == 'UNAVAILABLE' or vouchers.spawn[center] do - it = it + 1 - center = pseudorandom_element(_pool, pseudoseed(_pool_key..'_resample'..it)) - end + print "get voucher pls" + local center = SMODS.poll_object({pool = _pool}) vouchers[#vouchers+1] = center vouchers.spawn[center] = true @@ -3203,7 +3199,7 @@ function CardArea:handle_card_limit() trigger = 'immediate', func = function() if (self.config.card_limits.total_slots - self.config.card_count - (SMODS.cards_to_draw or 0)) > 0 and #G.deck.cards > (SMODS.cards_to_draw or 0) then - G.FUNCS.draw_from_deck_to_hand(self.config.card_limits.total_slots - self.config.card_count - (SMODS.cards_to_draw or 0)) + G.FUNCS.draw_from_deck_to_hand() end return true end @@ -3213,7 +3209,7 @@ function CardArea:handle_card_limit() })) elseif G.STATE == G.STATES.SELECTING_HAND and #G.deck.cards > 0 and self.config.card_limits.old_slots < self.config.card_limits.total_slots then if (self.config.card_limits.total_slots - self.config.card_limits.old_slots) > 0 then - G.FUNCS.draw_from_deck_to_hand((self.config.card_limits.total_slots - self.config.card_limits.old_slots)) + G.FUNCS.draw_from_deck_to_hand() end end if self == G.hand and G.STATE == G.STATES.SELECTING_HAND or G.STATE == G.STATES.DRAW_TO_HAND then diff --git a/src/utils/weights.lua b/src/utils/weights.lua index 6fa391d37..a8c1aa705 100644 --- a/src/utils/weights.lua +++ b/src/utils/weights.lua @@ -10,15 +10,16 @@ function SMODS.poll_object(args) -- Prepare pool local pool = args.pool or {} local types = args.labels or {args.type} - local total_weight = 0 - local modded_weight = 0 - local chance = (args.guaranteed and 1 or args.chance or SMODS.base_rate_percentage[args.type] or 1) * (args.mod or 1) - local poll_key = pseudorandom(pseudoseed(args.seed or SMODS.get_poll_key(args.type))) + print('Polling', table_as_string(types)) + + -- Populate pool + local types_used = {} if not args.pool then for _, label in ipairs(types) do + types_used[label] = true local temp_pool = {} for i=1, #(args.rarities or {true}) do - local _p = label == 'Blind' and get_new_boss(true) or get_current_pool(label, args.rarities and args.rarities[i]) + local _p = label == 'Blind' and SMODS.create_blind_pool(args.blind_type or 'boss') or get_current_pool(label, args.rarities and args.rarities[i]) if label == 'Edition' then local _options = {} for _, edition in ipairs(_p) do @@ -43,54 +44,80 @@ function SMODS.poll_object(args) -- Check pool has valid options assert(#pool > 0, "SMODS.poll_object called with an empty pool."..SMODS.log_crash_info(debug.getinfo(2))) - local final_pool = {} + local total_weight = 0 + local weight_pool = {} for _, key in ipairs(pool) do local weight_table = {} local w, m_w = SMODS.get_weight_of_object(G[SMODS.game_table_from_type[key.type] or 'P_CENTERS'][key.key or key], key.weight) - modded_weight = modded_weight + m_w - weight_table = {key = key.key or key, weight = w, mod_weight = modded_weight} + weight_table = {key = key.key or key, weight = m_w} + + total_weight = total_weight + w + weight_pool[#weight_pool + 1] = weight_table + weight_pool[key.key or key] = weight_table - total_weight = total_weight + weight_table.weight - table.insert(final_pool, weight_table) - if args.print then print(string.format("Key: %s, Weight: %s, Modded Weight: %s", weight_table.key, weight_table.weight, weight_table.mod_weight)) end + if args.print then print(string.format("Key: %s, Base weight: %s, Final weight: %s", weight_table.key, w, weight_table.weight)) end end - - if args.print then print('Total Weight: '..total_weight) end - if args.print then print('Modded Weight:'..modded_weight) end - if args.print then print('Base Chance: '..chance) end + -- Allow calculate functions to modify the pool table + SMODS.calculate_context({modify_weights = true, pool = weight_pool, pool_types = types_used}) + local modded_weight = 0 + -- Prepare final table to poll + local final_pool = {} + for _, weight_table in ipairs(weight_pool) do + modded_weight = modded_weight + weight_pool[weight_table.key].weight + weight_table.mod_weight = modded_weight + final_pool[#final_pool + 1] = weight_table + if args.print then print(string.format("Key: %s, Weight: %s, Position: %s", weight_table.key, weight_table.weight, weight_table.mod_weight)) end + end + + local chance = args.guaranteed and 1 or ((args.chance or SMODS.base_rate_percentage[args.type] or 1) * (args.mod or 1) * (modded_weight/total_weight)) -- Adjust chance based on modified weightings - chance = chance * (modded_weight/total_weight) - if args.print then print('Mod Chance: '..chance) end - if args.print then print('Poll Key:'..poll_key) end + -- chance = chance * (modded_weight/total_weight) + local key = 'UNAVAILABLE' + while key == 'UNAVAILABLE' do + local poll_key = pseudorandom(pseudoseed(args.seed or SMODS.get_poll_key(args.type))) + + if args.print then print('Total Weight: '..total_weight) end + if args.print then print('Modded Weight:'..modded_weight) end + if args.print then print('Base Chance: '..chance) end - if poll_key < (1 - chance) then - if args.print then print('Poll failed') end - return - end + if args.print then print('Mod Chance: '..chance) end + if args.print then print('Poll Key:'..poll_key) end - if not SMODS.no_repoll[args.type] then - poll_key = pseudorandom(pseudoseed(args.type_key or SMODS.get_poll_key(args.type, args.append or 'type'))) - if args.print then print('Poll key string:', args.type_key or SMODS.get_poll_key(args.type, args.append or 'type')) end - chance = 1 - end - if args.print then print('Poll key: '..poll_key) end + if poll_key < (1 - chance) then + if args.print then print('Poll failed') end + return + end - -- Find weight - local poll_weight = modded_weight - (poll_key - (1 - chance))/chance * modded_weight - if args.print then print('Looking for item: '..poll_weight) end - local low = 1 - local high = #final_pool - local ind = 1 - if poll_weight > final_pool[1].mod_weight then ind = SMODS.select_by_weight(final_pool, poll_weight, low, high) end - -- print('Index: '..ind) - -- print(final_pool[ind].key) + if not SMODS.no_repoll[args.type] then + poll_key = pseudorandom(pseudoseed(args.type_key or SMODS.get_poll_key(args.type, args.append or 'type'))) + if args.print then print('Poll key string:', args.type_key or SMODS.get_poll_key(args.type, args.append or 'type')) end + chance = 1 + end + if args.print then print('Poll key: '..poll_key) end - if args.no_negative and final_pool[ind].key == 'e_negative' then return 'e_polychrome' end + -- Find weight + local poll_weight = modded_weight - (poll_key - (1 - chance))/chance * modded_weight + if args.print then print('Looking for item: '..poll_weight) end + + if poll_weight > final_pool[1].mod_weight then + key = final_pool[SMODS.select_by_weight(final_pool, poll_weight, 1, #final_pool)].key + else + key = final_pool[1].key + end + end + -- Edition specific functionality + if args.no_negative and key == 'e_negative' then return 'e_polychrome' end + print("Result: "..key) + return key +end - return final_pool[ind].key +function table_as_string(t) + local str = '' + for _, v in ipairs(t) do str = str .. v .. ', ' end + return str end -- Returns the `weight` and `modified_weight` or a given object @@ -149,11 +176,11 @@ function SMODS.create_blind_pool(type, skip_cull) local res, options = SMODS.add_to_pool(v) options = options or {} if not v[type] then - elseif options.ignore_showdown_check then + elseif v.ignore_showdown_check or options.ignore_showdown_check then eligible_bosses[k] = res and true or nil elseif type == 'boss' then if - ((SMODS.is_showdown_ante()) == (v.boss.showdown or false)) and ((v[type].min or G.GAME.round_resets.ante) <= math.max(1, G.GAME.round_resets.ante)) and ((v[type].max or G.GAME.round_resets.ante) >= G.GAME.round_resets.ante) + ((SMODS.is_showdown_ante()) == (v[type].showdown or false)) and ((v[type].min or G.GAME.round_resets.ante) <= math.max(1, G.GAME.round_resets.ante)) and ((v[type].max or G.GAME.round_resets.ante) >= G.GAME.round_resets.ante) then eligible_bosses[k] = res and true or nil end @@ -167,7 +194,13 @@ function SMODS.create_blind_pool(type, skip_cull) if eligible_bosses[k] then eligible_bosses[k] = nil end end - if skip_cull then return eligible_bosses end + if skip_cull then + local final_pool = {} + for k, _ in pairs(eligible_bosses) do + final_pool[#final_pool + 1] = k + end + return final_pool + end local min_use = 100 for k, v in pairs(G.GAME.bosses_used) do @@ -178,17 +211,130 @@ function SMODS.create_blind_pool(type, skip_cull) end end end + local final_pool = {} for k, v in pairs(eligible_bosses) do if eligible_bosses[k] then if eligible_bosses[k] > min_use then eligible_bosses[k] = nil + else + final_pool[#final_pool + 1] = k end end end - return eligible_bosses + return final_pool end function SMODS.is_showdown_ante() return G.GAME.round_resets.ante%G.GAME.win_ante == 0 and G.GAME.round_resets.ante > 0 -end \ No newline at end of file +end + +-- New create_card_for_shop structure +function create_card_for_shop(area) + -- Tutorial Override + if area == G.shop_jokers and G.SETTINGS.tutorial_progress and G.SETTINGS.tutorial_progress.forced_shop and G.SETTINGS.tutorial_progress.forced_shop[#G.SETTINGS.tutorial_progress.forced_shop] then + local t = G.SETTINGS.tutorial_progress.forced_shop + local _center = G.P_CENTERS[t[#t]] or G.P_CENTERS.c_empress + local card = Card(area.T.x + area.T.w/2, area.T.y, G.CARD_W, G.CARD_H, G.P_CARDS.empty, _center, {bypass_discovery_center = true, bypass_discovery_ui = true}) + t[#t] = nil + if not t[1] then G.SETTINGS.tutorial_progress.forced_shop = nil end + + create_shop_card_ui(card) + return card + end + -- Tags that affect shop override + local forced_tag = nil + for k, v in ipairs(G.GAME.tags) do + if not forced_tag then + forced_tag = v:apply_to_run({type = 'store_joker_create', area = area}) + if forced_tag then + for kk, vv in ipairs(G.GAME.tags) do + if vv:apply_to_run({type = 'store_joker_modify', card = forced_tag}) then break end + end + return forced_tag + end + end + end + + -- Poll a type for the shop + local card_args = { + type = SMODS.poll_object_type({seed = 'cdt'..G.GAME.round_resets.ante}), + area = area + } + card_args.key = SMODS.poll_object({type = card_args.type, append = 'sho'}) + + local flags = SMODS.calculate_context({create_shop_card = true, set = card_args.type, key = card_args.key}) + + local card = SMODS.create_card(SMODS.merge_defaults(flags.shop_create_flags or {}, card_args)) + + SMODS.calculate_context({modify_shop_card = true, card = card}) + + create_shop_card_ui(card) + + -- Tag modifier check + G.E_MANAGER:add_event(Event({ + func = (function() + for k, v in ipairs(G.GAME.tags) do + if v:apply_to_run({type = 'store_joker_modify', card = card}) then break end + end + return true + end) + })) + + if (card.ability.set == 'Default' or card.ability.set == 'Enhanced') and G.GAME.used_vouchers["v_illusion"] and pseudorandom(pseudoseed('illusion')) > 0.8 then + card:set_edition(poll_edition('illusion', nil, true, true)) + end + + return card +end + +function SMODS.poll_object_type(args) + print "Using SMODS.poll_object_type" + args = args or {} + + -- If no types are given to select between, populate the list with all valid types + if not args.types then + args.types = { + 'Joker', 'playing_card', + } + for _,v in ipairs(SMODS.ConsumableType.obj_buffer) do + args.types[#args.types + 1] = v + end + else + -- Ensure types are in correct format + assert(type(args.types) == 'table', "SMODS.poll_object_type called with invalid types table."..SMODS.log_crash_info(debug.getinfo(2))) + end + + local total_rate = 0 + local weighted_table = {} + -- Populate `weighted_table` by finding the rates in G.GAME + for _, type in ipairs(args.types) do + total_rate = total_rate + G.GAME[type:lower()..'_rate'] + weighted_table[#weighted_table + 1] = {type = type, rate = G.GAME[type:lower()..'_rate'], mod_weight = total_rate} + + -- Playing Card modify type between Base and Enhanced + if type == 'playing_card' then weighted_table[#weighted_table].type = (G.GAME.used_vouchers["v_illusion"] and pseudorandom(pseudoseed('illusion')) > 0.6) and 'Enhanced' or 'Base' end + + if args.print then print(string.format("Type: %s, Weight: %s, Position: %s", type, G.GAME[type:lower()..'_rate'], total_rate)) end + end + + -- Adjust the pseudorandom number by the total_rate to obtain a number to check against the `mod_weight` values + local poll_weight = pseudorandom(args.seed or 'smods_poll_object_type') * total_rate + + if args.print then print('Looking for item: '..poll_weight) end + + local ind = 1 + -- If first element is not target, find correct index + if poll_weight > weighted_table[1].mod_weight then ind = SMODS.select_by_weight(weighted_table, poll_weight, 1, #weighted_table) end + + return weighted_table[ind].type +end + +-- function get_pack(_key, _type) +-- if not G.GAME.first_shop_buffoon and not G.GAME.banned_keys['p_buffoon_normal_1'] then +-- G.GAME.first_shop_buffoon = true +-- return G.P_CENTERS['p_buffoon_normal_'..(math.random(1, 2))] +-- end + +-- return G.P_CENTERS[SMODS.poll_object({type = 'Booster', seed = 'sho'..G.GAME.round_resets.ante})] +-- end \ No newline at end of file From 90ccac1c276be7578505bbb4627fed78b7108036 Mon Sep 17 00:00:00 2001 From: Eremel Date: Sat, 28 Mar 2026 11:50:31 +0000 Subject: [PATCH 06/32] Begin replacing vanilla polling --- lovely/weights.toml | 39 ++++++++++++------- src/overrides.lua | 2 +- src/utils.lua | 3 +- src/utils/weights.lua | 89 ++++++++++++++++++++++++------------------- 4 files changed, 77 insertions(+), 56 deletions(-) diff --git a/lovely/weights.toml b/lovely/weights.toml index e937aa3d3..8a48f0364 100644 --- a/lovely/weights.toml +++ b/lovely/weights.toml @@ -22,19 +22,6 @@ end ''' -# Add early_return argument to get_new_boss -[[patches]] -[patches.pattern] -target = 'functions/common_events.lua' -match_indent = true -position = 'at' -pattern = ''' -function get_new_boss() -''' -payload = ''' -function get_new_boss(early_return) -''' - # Adjust vanilla blinds max ante property [[patches]] [patches.pattern] @@ -83,3 +70,29 @@ payload = ''' print(_pool_key..'_resample'..it) print(_pool) ''' + +# Bypass get_new_boss() +[[patches]] +[patches.pattern] +target = 'functions/common_events.lua' +match_indent = true +position = 'after' +pattern = ''' +function get_new_boss() +''' +payload = ''' +if SMODS.optional_features.object_weights then return SMODS.WEIGHTS.poll_object({type = 'Blind'}) end +''' + +# Bypass create_card_for_shop() +[[patches]] +[patches.pattern] +target = 'functions/UI_definitions.lua' +match_indent = true +position = 'after' +pattern = ''' +function create_card_for_shop(area) +''' +payload = ''' +if SMODS.optional_features.object_weights then return SMODS.WEIGHTS.create_shop_card(area) end +''' diff --git a/src/overrides.lua b/src/overrides.lua index e7b792495..0da323b9d 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -2225,7 +2225,7 @@ function poll_edition(_key, _mod, _no_neg, _guaranteed, _options) _options = { 'e_negative', 'e_polychrome', 'e_holo', 'e_foil' } end - return SMODS.poll_object({type = 'Edition', seed = _key, guaranteed = _guaranteed, pool = _options, no_negative = _no_neg, mod = _mod}) + return SMODS.WEIGHTS.poll_object({type = 'Edition', seed = _key, guaranteed = _guaranteed, pool = _options, no_negative = _no_neg, mod = _mod}) -- REMOVED FOR NEW FUNCTION SMODS.poll_object diff --git a/src/utils.lua b/src/utils.lua index f574f1d0e..c89d4cee9 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -2330,8 +2330,7 @@ function SMODS.get_next_vouchers(vouchers) vouchers = vouchers or {spawn = {}} local _pool, _pool_key = get_current_pool('Voucher') for i=#vouchers+1, math.min(SMODS.size_of_pool(_pool), G.GAME.starting_params.vouchers_in_shop + (G.GAME.modifiers.extra_vouchers or 0)) do - print "get voucher pls" - local center = SMODS.poll_object({pool = _pool}) + local center = SMODS.WEIGHTS.poll_object({type = 'Voucher'}) vouchers[#vouchers+1] = center vouchers.spawn[center] = true diff --git a/src/utils/weights.lua b/src/utils/weights.lua index a8c1aa705..cd717c6aa 100644 --- a/src/utils/weights.lua +++ b/src/utils/weights.lua @@ -1,11 +1,13 @@ +SMODS.WEIGHTS = {} + -- TODO: labels are union or interset -- TODO: filter function accepted -- Returns a `key` of the polled object type ---@param args table|{type: string?, labels: table[string]?, pool: table[string]?, seed: string?, chance: number?, guaranteed: boolean?} -function SMODS.poll_object(args) - assert(args, "SMODS.poll_object called with no args."..SMODS.log_crash_info(debug.getinfo(2))) - assert((args.type or (args.labels and type(args.labels) == 'table') or (args.pool and type(args.pool) == 'table')), "SMODS.poll_object called without a pool source." .. SMODS.log_crash_info(debug.getinfo(2))) +function SMODS.WEIGHTS.poll_object(args) + assert(args, "SMODS.WEIGHTS.poll_object called with no args."..SMODS.log_crash_info(debug.getinfo(2))) + assert((args.type or (args.labels and type(args.labels) == 'table') or (args.pool and type(args.pool) == 'table')), "SMODS.WEIGHTS.poll_object called without a pool source." .. SMODS.log_crash_info(debug.getinfo(2))) -- Prepare pool local pool = args.pool or {} @@ -18,6 +20,7 @@ function SMODS.poll_object(args) for _, label in ipairs(types) do types_used[label] = true local temp_pool = {} + local join_func = args.intersect and SMODS.WEIGHTS.intersect_lists or SMODS.merge_lists for i=1, #(args.rarities or {true}) do local _p = label == 'Blind' and SMODS.create_blind_pool(args.blind_type or 'boss') or get_current_pool(label, args.rarities and args.rarities[i]) if label == 'Edition' then @@ -31,10 +34,10 @@ function SMODS.poll_object(args) end _p = _options end - temp_pool = SMODS.merge_lists({temp_pool, _p}) + temp_pool = join_func({temp_pool, _p}) end for _, v in ipairs(temp_pool) do - if G[SMODS.game_table_from_type[label] or 'P_CENTERS'][v] then table.insert(pool, {key = v, type = label}) end + if G[SMODS.WEIGHTS.game_table_from_type[label] or 'P_CENTERS'][v] then table.insert(pool, {key = v, type = label}) end end end end @@ -42,14 +45,14 @@ function SMODS.poll_object(args) if args.filter then pool = args.filter(pool) end -- Check pool has valid options - assert(#pool > 0, "SMODS.poll_object called with an empty pool."..SMODS.log_crash_info(debug.getinfo(2))) + assert(#pool > 0, "SMODS.WEIGHTS.poll_object called with an empty pool."..SMODS.log_crash_info(debug.getinfo(2))) local total_weight = 0 local weight_pool = {} for _, key in ipairs(pool) do local weight_table = {} - local w, m_w = SMODS.get_weight_of_object(G[SMODS.game_table_from_type[key.type] or 'P_CENTERS'][key.key or key], key.weight) + local w, m_w = SMODS.WEIGHTS.get_weight_of_object(G[SMODS.WEIGHTS.game_table_from_type[key.type] or 'P_CENTERS'][key.key or key], key.weight) weight_table = {key = key.key or key, weight = m_w} total_weight = total_weight + w @@ -72,12 +75,12 @@ function SMODS.poll_object(args) if args.print then print(string.format("Key: %s, Weight: %s, Position: %s", weight_table.key, weight_table.weight, weight_table.mod_weight)) end end - local chance = args.guaranteed and 1 or ((args.chance or SMODS.base_rate_percentage[args.type] or 1) * (args.mod or 1) * (modded_weight/total_weight)) + local chance = args.guaranteed and 1 or ((args.chance or SMODS.WEIGHTS.base_rate_percentage[args.type] or 1) * (args.mod or 1) * (modded_weight/total_weight)) -- Adjust chance based on modified weightings -- chance = chance * (modded_weight/total_weight) local key = 'UNAVAILABLE' while key == 'UNAVAILABLE' do - local poll_key = pseudorandom(pseudoseed(args.seed or SMODS.get_poll_key(args.type))) + local poll_key = pseudorandom(pseudoseed(args.seed or SMODS.WEIGHTS.get_poll_key(args.type))) if args.print then print('Total Weight: '..total_weight) end if args.print then print('Modded Weight:'..modded_weight) end @@ -91,19 +94,19 @@ function SMODS.poll_object(args) return end - if not SMODS.no_repoll[args.type] then - poll_key = pseudorandom(pseudoseed(args.type_key or SMODS.get_poll_key(args.type, args.append or 'type'))) - if args.print then print('Poll key string:', args.type_key or SMODS.get_poll_key(args.type, args.append or 'type')) end - chance = 1 - end - if args.print then print('Poll key: '..poll_key) end + if not SMODS.WEIGHTS.no_repoll[args.type] then + poll_key = pseudorandom(pseudoseed(args.type_key or SMODS.WEIGHTS.get_poll_key(args.type, args.append or 'type'))) + if args.print then print('Poll key string:', args.type_key or SMODS.WEIGHTS.get_poll_key(args.type, args.append or 'type')) end + chance = 1 + end + if args.print then print('Poll key: '..poll_key) end -- Find weight local poll_weight = modded_weight - (poll_key - (1 - chance))/chance * modded_weight if args.print then print('Looking for item: '..poll_weight) end if poll_weight > final_pool[1].mod_weight then - key = final_pool[SMODS.select_by_weight(final_pool, poll_weight, 1, #final_pool)].key + key = final_pool[SMODS.WEIGHTS.select_by_weight(final_pool, poll_weight, 1, #final_pool)].key else key = final_pool[1].key end @@ -122,7 +125,7 @@ end -- Returns the `weight` and `modified_weight` or a given object ---@param args table|{key: string, no_mod: boolean?} -function SMODS.get_weight_of_object(obj, opt_weight) +function SMODS.WEIGHTS.get_weight_of_object(obj, opt_weight) if not obj then return 10, 10 end local w = opt_weight or obj.weight or 10 local m = not opt_weight and obj.get_weight and obj:get_weight(w) or w @@ -130,7 +133,7 @@ function SMODS.get_weight_of_object(obj, opt_weight) return w, m end -function SMODS.select_by_weight(pool, poll, low, high, depth) +function SMODS.WEIGHTS.select_by_weight(pool, poll, low, high, depth) if high - low <= 1 then return high end local check = math.floor((low + high)/2) if poll < pool[check].mod_weight then @@ -138,20 +141,20 @@ function SMODS.select_by_weight(pool, poll, low, high, depth) else low = check end - return SMODS.select_by_weight(pool, poll, low, high, (depth or 0) + 1) + return SMODS.WEIGHTS.select_by_weight(pool, poll, low, high, (depth or 0) + 1) end -SMODS.base_rate_percentage = { +SMODS.WEIGHTS.base_rate_percentage = { Enhanced = 0.40, Seal = 0.02, Edition = 0.04 } -SMODS.no_repoll = { +SMODS.WEIGHTS.no_repoll = { Edition = true, } -SMODS.game_table_from_type = { +SMODS.WEIGHTS.game_table_from_type = { Seal = 'P_SEALS', Tag = 'P_TAGS', Blind = 'P_BLINDS', @@ -159,33 +162,34 @@ SMODS.game_table_from_type = { Stake = 'P_STAKES' } -SMODS.poll_keys = { +SMODS.WEIGHTS.poll_keys = { Edition = {str = 'edition_generic', block_infill = true}, Seal = {str = 'stdseal', ante = true}, Enhanced = {str = 'Enhanced', ante = true} } -function SMODS.get_poll_key(type, infill) - local t = SMODS.poll_keys[type] or {str = 'std_smods_poll', ante = true} +function SMODS.WEIGHTS.get_poll_key(type, infill) + local t = SMODS.WEIGHTS.poll_keys[type] or {str = 'std_smods_poll', ante = true} return t.str .. (t.block_infill and "" or infill or "") .. (t.ante and G.GAME.round_resets.ante or "") end -function SMODS.create_blind_pool(type, skip_cull) +function SMODS.create_blind_pool(blind_type, skip_cull) + assert(type(blind_type) == 'string', "SMODS.create_blind_pool called with a non-string type argument."..SMODS.log_crash_info(debug.getinfo(2))) local eligible_bosses = {} for k, v in pairs(G.P_BLINDS) do local res, options = SMODS.add_to_pool(v) options = options or {} - if not v[type] then - elseif v.ignore_showdown_check or options.ignore_showdown_check then + if not v[blind_type] then + elseif options.ignore_showdown_check then eligible_bosses[k] = res and true or nil - elseif type == 'boss' then + elseif blind_type == 'boss' then if - ((SMODS.is_showdown_ante()) == (v[type].showdown or false)) and ((v[type].min or G.GAME.round_resets.ante) <= math.max(1, G.GAME.round_resets.ante)) and ((v[type].max or G.GAME.round_resets.ante) >= G.GAME.round_resets.ante) + ((SMODS.is_showdown_ante()) == (v.boss.showdown or false)) and ((v[blind_type].min or G.GAME.round_resets.ante) <= math.max(1, G.GAME.round_resets.ante)) and ((v[blind_type].max or G.GAME.round_resets.ante) >= G.GAME.round_resets.ante) then eligible_bosses[k] = res and true or nil end else - if (v[type].min or G.GAME.round_resets.ante) <= math.max(1, G.GAME.round_resets.ante) and (v[type].max or G.GAME.round_resets.ante) >= G.GAME.round_resets.ante then + if (v[blind_type].min or G.GAME.round_resets.ante) <= math.max(1, G.GAME.round_resets.ante) and (v[blind_type].max or G.GAME.round_resets.ante) >= G.GAME.round_resets.ante then eligible_bosses[k] = res and true or nil end end @@ -221,8 +225,13 @@ function SMODS.create_blind_pool(type, skip_cull) end end end + + local output = {} + for k, _ in pairs(eligible_bosses) do + output[#output + 1] = k + end - return final_pool + return output end function SMODS.is_showdown_ante() @@ -230,7 +239,7 @@ function SMODS.is_showdown_ante() end -- New create_card_for_shop structure -function create_card_for_shop(area) +function SMODS.WEIGHTS.create_shop_card(area) -- Tutorial Override if area == G.shop_jokers and G.SETTINGS.tutorial_progress and G.SETTINGS.tutorial_progress.forced_shop and G.SETTINGS.tutorial_progress.forced_shop[#G.SETTINGS.tutorial_progress.forced_shop] then local t = G.SETTINGS.tutorial_progress.forced_shop @@ -258,10 +267,10 @@ function create_card_for_shop(area) -- Poll a type for the shop local card_args = { - type = SMODS.poll_object_type({seed = 'cdt'..G.GAME.round_resets.ante}), + type = SMODS.WEIGHTS.poll_object_type({seed = 'cdt'..G.GAME.round_resets.ante}), area = area } - card_args.key = SMODS.poll_object({type = card_args.type, append = 'sho'}) + card_args.key = SMODS.WEIGHTS.poll_object({type = card_args.type, append = 'sho'}) local flags = SMODS.calculate_context({create_shop_card = true, set = card_args.type, key = card_args.key}) @@ -288,8 +297,8 @@ function create_card_for_shop(area) return card end -function SMODS.poll_object_type(args) - print "Using SMODS.poll_object_type" +function SMODS.WEIGHTS.poll_object_type(args) + print "Using SMODS.WEIGHTS.poll_object_type" args = args or {} -- If no types are given to select between, populate the list with all valid types @@ -302,7 +311,7 @@ function SMODS.poll_object_type(args) end else -- Ensure types are in correct format - assert(type(args.types) == 'table', "SMODS.poll_object_type called with invalid types table."..SMODS.log_crash_info(debug.getinfo(2))) + assert(type(args.types) == 'table', "SMODS.WEIGHTS.poll_object_type called with invalid types table."..SMODS.log_crash_info(debug.getinfo(2))) end local total_rate = 0 @@ -325,7 +334,7 @@ function SMODS.poll_object_type(args) local ind = 1 -- If first element is not target, find correct index - if poll_weight > weighted_table[1].mod_weight then ind = SMODS.select_by_weight(weighted_table, poll_weight, 1, #weighted_table) end + if poll_weight > weighted_table[1].mod_weight then ind = SMODS.WEIGHTS.select_by_weight(weighted_table, poll_weight, 1, #weighted_table) end return weighted_table[ind].type end @@ -336,5 +345,5 @@ end -- return G.P_CENTERS['p_buffoon_normal_'..(math.random(1, 2))] -- end --- return G.P_CENTERS[SMODS.poll_object({type = 'Booster', seed = 'sho'..G.GAME.round_resets.ante})] +-- return G.P_CENTERS[SMODS.WEIGHTS.poll_object({type = 'Booster', seed = 'sho'..G.GAME.round_resets.ante})] -- end \ No newline at end of file From 3ab30069e8a955cc54999266996d8a26c7ff61ff Mon Sep 17 00:00:00 2001 From: Eremel Date: Sat, 28 Mar 2026 17:05:44 +0000 Subject: [PATCH 07/32] Finish vanilla replacement --- lovely/weights.toml | 31 +++++----- src/overrides.lua | 141 +++++++++++++++++++++--------------------- src/utils.lua | 35 ++++++++++- src/utils/weights.lua | 88 ++++++++++++++------------ 4 files changed, 169 insertions(+), 126 deletions(-) diff --git a/lovely/weights.toml b/lovely/weights.toml index 8a48f0364..3ae783339 100644 --- a/lovely/weights.toml +++ b/lovely/weights.toml @@ -57,42 +57,43 @@ payload = ''' if v.small or v.big then bosses_used[k] = 0 end ''' -# comment +# Bypass get_new_boss() [[patches]] [patches.pattern] target = 'functions/common_events.lua' match_indent = true -position = 'before' +position = 'after' pattern = ''' -center = pseudorandom_element(_pool, pseudoseed(_pool_key..'_resample'..it)) +function get_new_boss() ''' payload = ''' -print(_pool_key..'_resample'..it) -print(_pool) +if SMODS.optional_features.object_weights then return SMODS.poll_object({type = 'Blind'}) end ''' -# Bypass get_new_boss() +# Bypass create_card_for_shop() [[patches]] [patches.pattern] -target = 'functions/common_events.lua' +target = 'functions/UI_definitions.lua' match_indent = true position = 'after' pattern = ''' -function get_new_boss() +function create_card_for_shop(area) ''' payload = ''' -if SMODS.optional_features.object_weights then return SMODS.WEIGHTS.poll_object({type = 'Blind'}) end +if SMODS.optional_features.object_weights then return SMODS.create_shop_card(area) end ''' -# Bypass create_card_for_shop() +# Bypass create_card key selection [[patches]] [patches.pattern] -target = 'functions/UI_definitions.lua' +target = 'functions/common_events.lua' match_indent = true -position = 'after' +position = 'before' pattern = ''' -function create_card_for_shop(area) +if forced_key and not G.GAME.banned_keys[forced_key] then ''' payload = ''' -if SMODS.optional_features.object_weights then return SMODS.WEIGHTS.create_shop_card(area) end -''' +-- Use SMODS object weight system when enabled +if not forced_key and SMODS.optional_features.object_weights then forced_key = SMODS.poll_object({type = _type, guaranteed = true}) end + +''' \ No newline at end of file diff --git a/src/overrides.lua b/src/overrides.lua index 86b6dd5df..4f49725d9 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -2228,77 +2228,77 @@ function poll_edition(_key, _mod, _no_neg, _guaranteed, _options) _options = { 'e_negative', 'e_polychrome', 'e_holo', 'e_foil' } end - return SMODS.WEIGHTS.poll_object({type = 'Edition', seed = _key, guaranteed = _guaranteed, pool = _options, no_negative = _no_neg, mod = _mod}) + -- BYPASS REST OF FUNCTION WHEN WEIGHTS BEING USED + if SMODS.optional_features.object_weights then return SMODS.poll_object({type = 'Edition', seed = _key, guaranteed = _guaranteed, pool = _options, no_negative = _no_neg, mod = _mod}) end - -- REMOVED FOR NEW FUNCTION SMODS.poll_object - -- local _modifier = 1 - -- local edition_poll = pseudorandom(pseudoseed(_key or 'edition_generic')) -- Generate the poll value - -- local available_editions = {} -- Table containing a list of editions and their weights - - -- if not _options then - -- if _key == "wheel_of_fortune" or _key == "aura" then -- set base game edition polling - -- _options = { 'e_negative', 'e_polychrome', 'e_holo', 'e_foil' } - -- else - -- local unordered_options = get_current_pool("Edition", nil, nil, _key or 'edition_generic') - -- _options = {} - -- for _, edition in ipairs(unordered_options) do -- Flip the order of vanilla editions - -- if G.P_CENTERS[edition] and G.P_CENTERS[edition].vanilla then - -- table.insert(_options, 1, edition) - -- else - -- table.insert(_options, edition) - -- end - -- end - -- end - -- end - -- for _, v in ipairs(_options) do - -- local edition_option = {} - -- if type(v) == 'string' then - -- if v ~= 'UNAVAILABLE' then - -- assert(string.sub(v, 1, 2) == 'e_', ("Edition \"%s\" is missing \"e_\" prefix."):format(v)) - -- edition_option = { name = v, weight = G.P_CENTERS[v].weight } - -- table.insert(available_editions, edition_option) - -- end - -- elseif type(v) == 'table' then - -- assert(string.sub(v.name, 1, 2) == 'e_', ("Edition \"%s\" is missing \"e_\" prefix."):format(v.name)) - -- edition_option = { name = v.name, weight = v.weight } - -- table.insert(available_editions, edition_option) - -- end - -- end - - -- -- Calculate total weight of editions - -- local total_weight = 0 - -- for _, v in ipairs(available_editions) do - -- total_weight = total_weight + (v.weight) -- total all the weights of the polled editions - -- end - -- -- sendDebugMessage("Edition weights: "..total_weight, "EditionAPI") - -- -- If not guaranteed, calculate the base card rate to maintain base 4% chance of editions - -- if not _guaranteed then - -- _modifier = _mod or 1 - -- total_weight = total_weight + (total_weight / 4 * 96) -- Find total weight with base_card_rate as 96% - -- for _, v in ipairs(available_editions) do - -- v.weight = G.P_CENTERS[v.name]:get_weight() -- Apply game modifiers where appropriate (defined in edition declaration) - -- end - -- end - -- -- sendDebugMessage("Total weight: "..total_weight, "EditionAPI") - -- -- sendDebugMessage("Editions: "..#available_editions, "EditionAPI") - -- -- sendDebugMessage("Poll: "..edition_poll, "EditionAPI") - - -- -- Calculate whether edition is selected - -- local weight_i = 0 - -- for _, v in ipairs(available_editions) do - -- weight_i = weight_i + v.weight * _modifier - -- -- sendDebugMessage(v.name.." weight is "..v.weight*_modifier, "EditionAPI") - -- -- sendDebugMessage("Checking for "..v.name.." at "..(1 - (weight_i)/total_weight), "EditionAPI") - -- if edition_poll > 1 - (weight_i) / total_weight then - -- if not (v.name == 'e_negative' and _no_neg) then -- skip return if negative is selected and _no_neg is true - -- -- sendDebugMessage("Matched edition: "..v.name, "EditionAPI") - -- return v.name - -- end - -- end - -- end - - -- return nil + local _modifier = 1 + local edition_poll = pseudorandom(pseudoseed(_key or 'edition_generic')) -- Generate the poll value + local available_editions = {} -- Table containing a list of editions and their weights + + if not _options then + if _key == "wheel_of_fortune" or _key == "aura" then -- set base game edition polling + _options = { 'e_negative', 'e_polychrome', 'e_holo', 'e_foil' } + else + local unordered_options = get_current_pool("Edition", nil, nil, _key or 'edition_generic') + _options = {} + for _, edition in ipairs(unordered_options) do -- Flip the order of vanilla editions + if G.P_CENTERS[edition] and G.P_CENTERS[edition].vanilla then + table.insert(_options, 1, edition) + else + table.insert(_options, edition) + end + end + end + end + for _, v in ipairs(_options) do + local edition_option = {} + if type(v) == 'string' then + if v ~= 'UNAVAILABLE' then + assert(string.sub(v, 1, 2) == 'e_', ("Edition \"%s\" is missing \"e_\" prefix."):format(v)) + edition_option = { name = v, weight = G.P_CENTERS[v].weight } + table.insert(available_editions, edition_option) + end + elseif type(v) == 'table' then + assert(string.sub(v.name, 1, 2) == 'e_', ("Edition \"%s\" is missing \"e_\" prefix."):format(v.name)) + edition_option = { name = v.name, weight = v.weight } + table.insert(available_editions, edition_option) + end + end + + -- Calculate total weight of editions + local total_weight = 0 + for _, v in ipairs(available_editions) do + total_weight = total_weight + (v.weight) -- total all the weights of the polled editions + end + -- sendDebugMessage("Edition weights: "..total_weight, "EditionAPI") + -- If not guaranteed, calculate the base card rate to maintain base 4% chance of editions + if not _guaranteed then + _modifier = _mod or 1 + total_weight = total_weight + (total_weight / 4 * 96) -- Find total weight with base_card_rate as 96% + for _, v in ipairs(available_editions) do + v.weight = G.P_CENTERS[v.name]:get_weight() -- Apply game modifiers where appropriate (defined in edition declaration) + end + end + -- sendDebugMessage("Total weight: "..total_weight, "EditionAPI") + -- sendDebugMessage("Editions: "..#available_editions, "EditionAPI") + -- sendDebugMessage("Poll: "..edition_poll, "EditionAPI") + + -- Calculate whether edition is selected + local weight_i = 0 + for _, v in ipairs(available_editions) do + weight_i = weight_i + v.weight * _modifier + -- sendDebugMessage(v.name.." weight is "..v.weight*_modifier, "EditionAPI") + -- sendDebugMessage("Checking for "..v.name.." at "..(1 - (weight_i)/total_weight), "EditionAPI") + if edition_poll > 1 - (weight_i) / total_weight then + if not (v.name == 'e_negative' and _no_neg) then -- skip return if negative is selected and _no_neg is true + -- sendDebugMessage("Matched edition: "..v.name, "EditionAPI") + return v.name + end + end + end + + return nil end -- local cge = Card.get_edition @@ -2447,6 +2447,9 @@ function get_pack(_key, _type) G.GAME.first_shop_buffoon = true return G.P_CENTERS['p_buffoon_normal_'..(math.random(1, 2))] end + if SMODS.optional_features.object_weights then + return G.P_CENTERS[SMODS.poll_object({type = 'Booster'})] + end local cume, it, center = 0, 0, nil local temp_in_pool = {} for k, v in ipairs(G.P_CENTER_POOLS['Booster']) do diff --git a/src/utils.lua b/src/utils.lua index 86a1d2d18..be5a82904 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -672,6 +672,26 @@ function SMODS.merge_lists(...) return ret end +-- Flatten the given arrays of arrays into one, then +-- add any duplicate values to a new table in order +function SMODS.intersect_lists(lists) + local function find_intersects(l1, l2) + local seen = {} + local ret = {} + for _, v in ipairs(l1) do seen[v] = true end + for _, v in ipairs(l2) do if seen[v] then ret[#ret + 1] = v end end + + return ret + end + + local output = {} + for i=1, #lists - 1 do + output = find_intersects(lists[i], lists[i+1]) + end + + return output +end + --#region Number formatting function round_number(num, precision) @@ -697,6 +717,9 @@ function SMODS.poll_edition(args) end function SMODS.poll_seal(args) + -- BYPASS REST OF FUNCTION WHEN WEIGHTS BEING USED + if SMODS.optional_features.object_weights then args.type = 'Seal'; return SMODS.poll_object(args) end + args = args or {} local key = args.key or 'stdseal' local mod = args.mod or 1 @@ -2415,7 +2438,17 @@ function SMODS.get_next_vouchers(vouchers) vouchers = vouchers or {spawn = {}} local _pool, _pool_key = get_current_pool('Voucher') for i=#vouchers+1, math.min(SMODS.size_of_pool(_pool), G.GAME.starting_params.vouchers_in_shop + (G.GAME.modifiers.extra_vouchers or 0)) do - local center = SMODS.WEIGHTS.poll_object({type = 'Voucher'}) + local center + if SMODS.optional_features.object_weights then + center = SMODS.poll_object({type = 'Voucher'}) + else + center = pseudorandom_element(_pool, pseudoseed(_pool_key)) + local it = 1 + while center == 'UNAVAILABLE' or vouchers.spawn[center] do + it = it + 1 + center = pseudorandom_element(_pool, pseudoseed(_pool_key..'_resample'..it)) + end + end vouchers[#vouchers+1] = center vouchers.spawn[center] = true diff --git a/src/utils/weights.lua b/src/utils/weights.lua index cd717c6aa..bb3cd2331 100644 --- a/src/utils/weights.lua +++ b/src/utils/weights.lua @@ -1,18 +1,23 @@ -SMODS.WEIGHTS = {} - -- TODO: labels are union or interset -- TODO: filter function accepted -- Returns a `key` of the polled object type ---@param args table|{type: string?, labels: table[string]?, pool: table[string]?, seed: string?, chance: number?, guaranteed: boolean?} -function SMODS.WEIGHTS.poll_object(args) - assert(args, "SMODS.WEIGHTS.poll_object called with no args."..SMODS.log_crash_info(debug.getinfo(2))) - assert((args.type or (args.labels and type(args.labels) == 'table') or (args.pool and type(args.pool) == 'table')), "SMODS.WEIGHTS.poll_object called without a pool source." .. SMODS.log_crash_info(debug.getinfo(2))) +function SMODS.poll_object(args) + assert(args, "SMODS.poll_object called with no args."..SMODS.log_crash_info(debug.getinfo(2))) + assert((args.type or (args.labels and type(args.labels) == 'table') or (args.pool and type(args.pool) == 'table')), "SMODS.poll_object called without a pool source." .. SMODS.log_crash_info(debug.getinfo(2))) + + -- TODO: remove before merge + local function table_as_string(t) + local str = '' + for _, v in ipairs(t) do str = str .. v .. ', ' end + return str + end -- Prepare pool local pool = args.pool or {} local types = args.labels or {args.type} - print('Polling', table_as_string(types)) + if SMODS.debug_prints then print('Polling', table_as_string(types)) end -- Populate pool local types_used = {} @@ -20,7 +25,7 @@ function SMODS.WEIGHTS.poll_object(args) for _, label in ipairs(types) do types_used[label] = true local temp_pool = {} - local join_func = args.intersect and SMODS.WEIGHTS.intersect_lists or SMODS.merge_lists + local join_func = args.intersect and SMODS.intersect_lists or SMODS.merge_lists for i=1, #(args.rarities or {true}) do local _p = label == 'Blind' and SMODS.create_blind_pool(args.blind_type or 'boss') or get_current_pool(label, args.rarities and args.rarities[i]) if label == 'Edition' then @@ -37,7 +42,7 @@ function SMODS.WEIGHTS.poll_object(args) temp_pool = join_func({temp_pool, _p}) end for _, v in ipairs(temp_pool) do - if G[SMODS.WEIGHTS.game_table_from_type[label] or 'P_CENTERS'][v] then table.insert(pool, {key = v, type = label}) end + if G[SMODS.game_table_from_type[label] or 'P_CENTERS'][v] then table.insert(pool, {key = v, type = label}) end end end end @@ -45,14 +50,14 @@ function SMODS.WEIGHTS.poll_object(args) if args.filter then pool = args.filter(pool) end -- Check pool has valid options - assert(#pool > 0, "SMODS.WEIGHTS.poll_object called with an empty pool."..SMODS.log_crash_info(debug.getinfo(2))) + assert(#pool > 0, "SMODS.poll_object called with an empty pool."..SMODS.log_crash_info(debug.getinfo(2))) local total_weight = 0 local weight_pool = {} for _, key in ipairs(pool) do local weight_table = {} - local w, m_w = SMODS.WEIGHTS.get_weight_of_object(G[SMODS.WEIGHTS.game_table_from_type[key.type] or 'P_CENTERS'][key.key or key], key.weight) + local w, m_w = SMODS.get_weight_of_object(G[SMODS.game_table_from_type[key.type] or 'P_CENTERS'][key.key or key], key.weight) weight_table = {key = key.key or key, weight = m_w} total_weight = total_weight + w @@ -75,12 +80,12 @@ function SMODS.WEIGHTS.poll_object(args) if args.print then print(string.format("Key: %s, Weight: %s, Position: %s", weight_table.key, weight_table.weight, weight_table.mod_weight)) end end - local chance = args.guaranteed and 1 or ((args.chance or SMODS.WEIGHTS.base_rate_percentage[args.type] or 1) * (args.mod or 1) * (modded_weight/total_weight)) + local chance = args.guaranteed and 1 or ((args.chance or SMODS.base_rate_percentage[args.type] or 1) * (args.mod or 1) * (modded_weight/total_weight)) -- Adjust chance based on modified weightings -- chance = chance * (modded_weight/total_weight) local key = 'UNAVAILABLE' while key == 'UNAVAILABLE' do - local poll_key = pseudorandom(pseudoseed(args.seed or SMODS.WEIGHTS.get_poll_key(args.type))) + local poll_key = pseudorandom(pseudoseed(args.seed or SMODS.get_poll_key(args.type))) if args.print then print('Total Weight: '..total_weight) end if args.print then print('Modded Weight:'..modded_weight) end @@ -91,12 +96,13 @@ function SMODS.WEIGHTS.poll_object(args) if poll_key < (1 - chance) then if args.print then print('Poll failed') end + if SMODS.debug_prints then print("Result: none") end return end - if not SMODS.WEIGHTS.no_repoll[args.type] then - poll_key = pseudorandom(pseudoseed(args.type_key or SMODS.WEIGHTS.get_poll_key(args.type, args.append or 'type'))) - if args.print then print('Poll key string:', args.type_key or SMODS.WEIGHTS.get_poll_key(args.type, args.append or 'type')) end + if not SMODS.no_repoll[args.type] then + poll_key = pseudorandom(pseudoseed(args.type_key or SMODS.get_poll_key(args.type, args.append or 'type'))) + if args.print then print('Poll key string:', args.type_key or SMODS.get_poll_key(args.type, args.append or 'type')) end chance = 1 end if args.print then print('Poll key: '..poll_key) end @@ -106,7 +112,7 @@ function SMODS.WEIGHTS.poll_object(args) if args.print then print('Looking for item: '..poll_weight) end if poll_weight > final_pool[1].mod_weight then - key = final_pool[SMODS.WEIGHTS.select_by_weight(final_pool, poll_weight, 1, #final_pool)].key + key = final_pool[SMODS.select_by_weight(final_pool, poll_weight, 1, #final_pool)].key else key = final_pool[1].key end @@ -117,15 +123,9 @@ function SMODS.WEIGHTS.poll_object(args) return key end -function table_as_string(t) - local str = '' - for _, v in ipairs(t) do str = str .. v .. ', ' end - return str -end - -- Returns the `weight` and `modified_weight` or a given object ---@param args table|{key: string, no_mod: boolean?} -function SMODS.WEIGHTS.get_weight_of_object(obj, opt_weight) +function SMODS.get_weight_of_object(obj, opt_weight) if not obj then return 10, 10 end local w = opt_weight or obj.weight or 10 local m = not opt_weight and obj.get_weight and obj:get_weight(w) or w @@ -133,7 +133,7 @@ function SMODS.WEIGHTS.get_weight_of_object(obj, opt_weight) return w, m end -function SMODS.WEIGHTS.select_by_weight(pool, poll, low, high, depth) +function SMODS.select_by_weight(pool, poll, low, high, depth) if high - low <= 1 then return high end local check = math.floor((low + high)/2) if poll < pool[check].mod_weight then @@ -141,20 +141,20 @@ function SMODS.WEIGHTS.select_by_weight(pool, poll, low, high, depth) else low = check end - return SMODS.WEIGHTS.select_by_weight(pool, poll, low, high, (depth or 0) + 1) + return SMODS.select_by_weight(pool, poll, low, high, (depth or 0) + 1) end -SMODS.WEIGHTS.base_rate_percentage = { +SMODS.base_rate_percentage = { Enhanced = 0.40, Seal = 0.02, Edition = 0.04 } -SMODS.WEIGHTS.no_repoll = { +SMODS.no_repoll = { Edition = true, } -SMODS.WEIGHTS.game_table_from_type = { +SMODS.game_table_from_type = { Seal = 'P_SEALS', Tag = 'P_TAGS', Blind = 'P_BLINDS', @@ -162,14 +162,14 @@ SMODS.WEIGHTS.game_table_from_type = { Stake = 'P_STAKES' } -SMODS.WEIGHTS.poll_keys = { +SMODS.poll_keys = { Edition = {str = 'edition_generic', block_infill = true}, Seal = {str = 'stdseal', ante = true}, Enhanced = {str = 'Enhanced', ante = true} } -function SMODS.WEIGHTS.get_poll_key(type, infill) - local t = SMODS.WEIGHTS.poll_keys[type] or {str = 'std_smods_poll', ante = true} +function SMODS.get_poll_key(type, infill) + local t = SMODS.poll_keys[type] or {str = 'std_smods_poll', ante = true} return t.str .. (t.block_infill and "" or infill or "") .. (t.ante and G.GAME.round_resets.ante or "") end @@ -239,7 +239,7 @@ function SMODS.is_showdown_ante() end -- New create_card_for_shop structure -function SMODS.WEIGHTS.create_shop_card(area) +function SMODS.create_shop_card(area) -- Tutorial Override if area == G.shop_jokers and G.SETTINGS.tutorial_progress and G.SETTINGS.tutorial_progress.forced_shop and G.SETTINGS.tutorial_progress.forced_shop[#G.SETTINGS.tutorial_progress.forced_shop] then local t = G.SETTINGS.tutorial_progress.forced_shop @@ -267,10 +267,10 @@ function SMODS.WEIGHTS.create_shop_card(area) -- Poll a type for the shop local card_args = { - type = SMODS.WEIGHTS.poll_object_type({seed = 'cdt'..G.GAME.round_resets.ante}), + type = SMODS.poll_object_type({seed = 'cdt'..G.GAME.round_resets.ante}), area = area } - card_args.key = SMODS.WEIGHTS.poll_object({type = card_args.type, append = 'sho'}) + card_args.key = SMODS.poll_object({type = card_args.type, append = 'sho'}) local flags = SMODS.calculate_context({create_shop_card = true, set = card_args.type, key = card_args.key}) @@ -297,8 +297,8 @@ function SMODS.WEIGHTS.create_shop_card(area) return card end -function SMODS.WEIGHTS.poll_object_type(args) - print "Using SMODS.WEIGHTS.poll_object_type" +function SMODS.poll_object_type(args) + if SMODS.debug_prints then print "Using SMODS.poll_object_type" end args = args or {} -- If no types are given to select between, populate the list with all valid types @@ -311,7 +311,7 @@ function SMODS.WEIGHTS.poll_object_type(args) end else -- Ensure types are in correct format - assert(type(args.types) == 'table', "SMODS.WEIGHTS.poll_object_type called with invalid types table."..SMODS.log_crash_info(debug.getinfo(2))) + assert(type(args.types) == 'table', "SMODS.poll_object_type called with invalid types table."..SMODS.log_crash_info(debug.getinfo(2))) end local total_rate = 0 @@ -334,8 +334,8 @@ function SMODS.WEIGHTS.poll_object_type(args) local ind = 1 -- If first element is not target, find correct index - if poll_weight > weighted_table[1].mod_weight then ind = SMODS.WEIGHTS.select_by_weight(weighted_table, poll_weight, 1, #weighted_table) end - + if poll_weight > weighted_table[1].mod_weight then ind = SMODS.select_by_weight(weighted_table, poll_weight, 1, #weighted_table) end + if SMODS.debug_prints then print(weighted_table[ind].type) end return weighted_table[ind].type end @@ -345,5 +345,11 @@ end -- return G.P_CENTERS['p_buffoon_normal_'..(math.random(1, 2))] -- end --- return G.P_CENTERS[SMODS.WEIGHTS.poll_object({type = 'Booster', seed = 'sho'..G.GAME.round_resets.ante})] --- end \ No newline at end of file +-- return G.P_CENTERS[SMODS.poll_object({type = 'Booster', seed = 'sho'..G.GAME.round_resets.ante})] +-- end + +local smods_get_voucher_key = get_next_voucher_key +function get_next_voucher_key(_from_tag) + if SMODS.optional_features.object_weights then return SMODS.poll_object({type = 'Voucher'}) end + return smods_get_voucher_key(_from_tag) +end From 901b042bd35c4b11cb239bd3fca74791e7c12a88 Mon Sep 17 00:00:00 2001 From: Eremel Date: Sat, 28 Mar 2026 23:14:15 +0000 Subject: [PATCH 08/32] Add intersecting pools support --- src/utils/weights.lua | 72 +++++++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/src/utils/weights.lua b/src/utils/weights.lua index bb3cd2331..cdfa13357 100644 --- a/src/utils/weights.lua +++ b/src/utils/weights.lua @@ -1,5 +1,7 @@ --- TODO: labels are union or interset --- TODO: filter function accepted +-- TODO: object labels SMODS class +-- TODO: populate vanilla object labels (chips, mult, xmult, food, space, generation, diamond, heart, spade, club) +-- TODO: object labels integrated into `get_current_pool` + -- Returns a `key` of the polled object type ---@param args table|{type: string?, labels: table[string]?, pool: table[string]?, seed: string?, chance: number?, guaranteed: boolean?} @@ -21,30 +23,9 @@ function SMODS.poll_object(args) -- Populate pool local types_used = {} + if not args.pool then - for _, label in ipairs(types) do - types_used[label] = true - local temp_pool = {} - local join_func = args.intersect and SMODS.intersect_lists or SMODS.merge_lists - for i=1, #(args.rarities or {true}) do - local _p = label == 'Blind' and SMODS.create_blind_pool(args.blind_type or 'boss') or get_current_pool(label, args.rarities and args.rarities[i]) - if label == 'Edition' then - local _options = {} - for _, edition in ipairs(_p) do - if G.P_CENTERS[edition] and G.P_CENTERS[edition].vanilla then - table.insert(_options, 1, edition) - elseif G.P_CENTERS[edition] then - table.insert(_options, edition) - end - end - _p = _options - end - temp_pool = join_func({temp_pool, _p}) - end - for _, v in ipairs(temp_pool) do - if G[SMODS.game_table_from_type[label] or 'P_CENTERS'][v] then table.insert(pool, {key = v, type = label}) end - end - end + pool, types_used = SMODS.create_poll_pool(types, args) end if args.filter then pool = args.filter(pool) end @@ -119,7 +100,7 @@ function SMODS.poll_object(args) end -- Edition specific functionality if args.no_negative and key == 'e_negative' then return 'e_polychrome' end - print("Result: "..key) + if SMODS.debug_prints then print("Result: "..key) end return key end @@ -234,6 +215,45 @@ function SMODS.create_blind_pool(blind_type, skip_cull) return output end +-- Create a table of {key = string, type = label} items to be polled +function SMODS.create_poll_pool(labels, args) + local labels_used = {} + local pool = {} + local final_pool + + for _, label in ipairs(labels) do + labels_used[label] = true + local temp_pool = {} + local join_func = args.intersect and SMODS.intersect_lists or SMODS.merge_lists + for i=1, #(args.rarities or {true}) do + local _p = label == 'Blind' and SMODS.create_blind_pool(args.blind_type or 'boss') or get_current_pool(label, args.rarities and args.rarities[i]) + if label == 'Edition' then + local _options = {} + for _, edition in ipairs(_p) do + if G.P_CENTERS[edition] and G.P_CENTERS[edition].vanilla then + table.insert(_options, 1, edition) + elseif G.P_CENTERS[edition] then + table.insert(_options, edition) + end + end + _p = _options + end + temp_pool = SMODS.merge_lists({temp_pool, _p}) + end + for _, v in ipairs(temp_pool) do + if G[SMODS.game_table_from_type[label] or 'P_CENTERS'][v] then pool[v] = {key = v, type = label} end + end + final_pool = final_pool and join_func({final_pool, temp_pool}) or temp_pool + end + + local ret_pool = {} + for i, k in ipairs(final_pool) do + table.insert(ret_pool, pool[k]) + end + + return ret_pool, labels_used +end + function SMODS.is_showdown_ante() return G.GAME.round_resets.ante%G.GAME.win_ante == 0 and G.GAME.round_resets.ante > 0 end From 561b3f5c299783165a08bb23ca5abc13cea282e1 Mon Sep 17 00:00:00 2001 From: Eremel Date: Sun, 29 Mar 2026 20:13:14 +0100 Subject: [PATCH 09/32] this shouldn't be here --- src/overrides.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/src/overrides.lua b/src/overrides.lua index 4f49725d9..e464aa66d 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -2067,7 +2067,6 @@ function Card:set_edition(edition, immediate, silent, delay) SMODS.enh_cache:write(self, nil) if self.edition then - print('hitting here') self.ability.card_limit = self.ability.card_limit - (self.edition.card_limit or 0) self.ability.extra_slots_used = self.ability.extra_slots_used - (self.edition.extra_slots_used or 0) end From 1cc5abc5a9b8b888b5e39e4d0caeff11ac099c3e Mon Sep 17 00:00:00 2001 From: Eremel Date: Sun, 29 Mar 2026 20:58:46 +0100 Subject: [PATCH 10/32] Add attributes support --- src/utils/weights.lua | 94 ++++++++++++++++++++++++++++++++----------- 1 file changed, 70 insertions(+), 24 deletions(-) diff --git a/src/utils/weights.lua b/src/utils/weights.lua index cdfa13357..74778d4df 100644 --- a/src/utils/weights.lua +++ b/src/utils/weights.lua @@ -4,10 +4,11 @@ -- Returns a `key` of the polled object type ----@param args table|{type: string?, labels: table[string]?, pool: table[string]?, seed: string?, chance: number?, guaranteed: boolean?} +---@param args table|{type: string?, attributes: table[string]?, pool: table[string]?, seed: string?, chance: number?, guaranteed: boolean?} function SMODS.poll_object(args) + assert(SMODS.optional_features.object_weights, "SMODS.poll_object called with object_weights optional feature disabled."..SMODS.log_crash_info(debug.getinfo(2))) assert(args, "SMODS.poll_object called with no args."..SMODS.log_crash_info(debug.getinfo(2))) - assert((args.type or (args.labels and type(args.labels) == 'table') or (args.pool and type(args.pool) == 'table')), "SMODS.poll_object called without a pool source." .. SMODS.log_crash_info(debug.getinfo(2))) + assert((args.type or (args.types and type(args.types) == 'table') or (args.attributes and type(args.attributes) == 'table') or (args.pool and type(args.pool) == 'table')), "SMODS.poll_object called without a pool source." .. SMODS.log_crash_info(debug.getinfo(2))) -- TODO: remove before merge local function table_as_string(t) @@ -18,7 +19,7 @@ function SMODS.poll_object(args) -- Prepare pool local pool = args.pool or {} - local types = args.labels or {args.type} + local types = args.attributes or args.types or {args.type} if SMODS.debug_prints then print('Polling', table_as_string(types)) end -- Populate pool @@ -81,12 +82,12 @@ function SMODS.poll_object(args) return end - if not SMODS.no_repoll[args.type] then - poll_key = pseudorandom(pseudoseed(args.type_key or SMODS.get_poll_key(args.type, args.append or 'type'))) - if args.print then print('Poll key string:', args.type_key or SMODS.get_poll_key(args.type, args.append or 'type')) end - chance = 1 - end - if args.print then print('Poll key: '..poll_key) end + if not SMODS.no_repoll[args.type] then + poll_key = pseudorandom(pseudoseed(args.type_key or SMODS.get_poll_key(args.type, args.append or 'type'))) + if args.print then print('Poll key string:', args.type_key or SMODS.get_poll_key(args.type, args.append or 'type')) end + chance = 1 + end + if args.print then print('Poll key: '..poll_key) end -- Find weight local poll_weight = modded_weight - (poll_key - (1 - chance))/chance * modded_weight @@ -98,10 +99,11 @@ function SMODS.poll_object(args) key = final_pool[1].key end end - -- Edition specific functionality - if args.no_negative and key == 'e_negative' then return 'e_polychrome' end - if SMODS.debug_prints then print("Result: "..key) end - return key + + -- Edition specific functionality + if args.no_negative and key == 'e_negative' then return 'e_polychrome' end + if SMODS.debug_prints then print("Result: "..key) end + return key end -- Returns the `weight` and `modified_weight` or a given object @@ -220,13 +222,26 @@ function SMODS.create_poll_pool(labels, args) local labels_used = {} local pool = {} local final_pool + local it = 0 + local function join_lists(args) + local l1 = args[1] or {} + local l2 = args[2] or {} + for _, v in ipairs(l2) do + l1[#l1 + 1] = v + end + return l1 + end + for _, label in ipairs(labels) do labels_used[label] = true local temp_pool = {} - local join_func = args.intersect and SMODS.intersect_lists or SMODS.merge_lists + local join_func = args.intersect and SMODS.intersect_lists or join_lists for i=1, #(args.rarities or {true}) do - local _p = label == 'Blind' and SMODS.create_blind_pool(args.blind_type or 'boss') or get_current_pool(label, args.rarities and args.rarities[i]) + local _p = label == 'Blind' and SMODS.create_blind_pool(args.blind_type or 'boss') or SMODS.Attributes[label] and SMODS.get_attribute_pool(label) or get_current_pool(label, args.rarities and args.rarities[i]) + if SMODS.Attributes[label] then + _p = SMODS.cull_pool(_p, label, args) + end if label == 'Edition' then local _options = {} for _, edition in ipairs(_p) do @@ -238,19 +253,23 @@ function SMODS.create_poll_pool(labels, args) end _p = _options end - temp_pool = SMODS.merge_lists({temp_pool, _p}) + temp_pool = join_lists({temp_pool, _p}) end for _, v in ipairs(temp_pool) do - if G[SMODS.game_table_from_type[label] or 'P_CENTERS'][v] then pool[v] = {key = v, type = label} end + pool[v] = {key = v, type = label} end final_pool = final_pool and join_func({final_pool, temp_pool}) or temp_pool end local ret_pool = {} + local pool_exists = false for i, k in ipairs(final_pool) do + if not pool_exists and k ~= 'UNAVAILABLE' then pool_exists = true end table.insert(ret_pool, pool[k]) end + if not pool_exists then ret_pool = {{key = 'j_joker', type = 'Joker'}} end + return ret_pool, labels_used end @@ -359,14 +378,41 @@ function SMODS.poll_object_type(args) return weighted_table[ind].type end --- function get_pack(_key, _type) --- if not G.GAME.first_shop_buffoon and not G.GAME.banned_keys['p_buffoon_normal_1'] then --- G.GAME.first_shop_buffoon = true --- return G.P_CENTERS['p_buffoon_normal_'..(math.random(1, 2))] --- end +function SMODS.cull_pool(pool, type, args) + local final_pool = {} + for _, key in ipairs(pool) do + local add = nil + local v = G.P_CENTERS[key] + local in_pool, pool_opts = SMODS.add_to_pool(v, { source = args.append }) + pool_opts = pool_opts or {} + if not (G.GAME.used_jokers[v.key] and not pool_opts.allow_duplicates and not SMODS.showman(v.key) and not args.allow_duplicates) and + (v.unlocked ~= false or v.rarity == 4) then + if v.enhancement_gate then + add = nil + for kk, vv in pairs(G.playing_cards) do + if SMODS.has_enhancement(vv, v.enhancement_gate) then + add = true + end + end + else + add = true + end + end --- return G.P_CENTERS[SMODS.poll_object({type = 'Booster', seed = 'sho'..G.GAME.round_resets.ante})] --- end + 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 + + add = in_pool and (add or pool_opts.override_base_checks) + + if add and not G.GAME.banned_keys[v.key] then + final_pool[#final_pool + 1] = v.key + else + final_pool[#final_pool + 1] = 'UNAVAILABLE' + end + end + + return final_pool +end local smods_get_voucher_key = get_next_voucher_key function get_next_voucher_key(_from_tag) From 4d88c26dd41a7eed2c6cab1c9bca9ed2e7c9799b Mon Sep 17 00:00:00 2001 From: Eremel Date: Sun, 29 Mar 2026 21:09:38 +0100 Subject: [PATCH 11/32] Initial attributes work --- src/game_object.lua | 13 +++++ src/game_objects/attributes.lua | 97 +++++++++++++++++++++++++++++++++ src/preflight/loader.lua | 1 + src/utils.lua | 4 +- 4 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 src/game_objects/attributes.lua diff --git a/src/game_object.lua b/src/game_object.lua index d9bf6c6fc..bf76a353d 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -389,6 +389,12 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. end, } + ------------------------------------------------------------------------------------------------- + ----- API CODE GameObject.Attribute + ------------------------------------------------------------------------------------------------- + + assert(load(SMODS.NFS.read(SMODS.path..'src/game_objects/attributes.lua'), ('=[SMODS _ "src/game_objects/attributes.lua"]')))() + ------------------------------------------------------------------------------------------------- ----- INTERNAL API CODE GameObject._Loc_Pre ------------------------------------------------------------------------------------------------- @@ -1188,6 +1194,13 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. v:inject_card(self) end end + if self.attributes then + for _, attribute in ipairs(self.attributes) do + if SMODS.Attributes[attribute] then + SMODS.Attributes[attribute].keys = SMODS.merge_lists({SMODS.Attributes[attribute].keys or {}, {self.key}}) + end + end + end end, delete = function(self) G.P_CENTERS[self.key] = nil diff --git a/src/game_objects/attributes.lua b/src/game_objects/attributes.lua new file mode 100644 index 000000000..012464adb --- /dev/null +++ b/src/game_objects/attributes.lua @@ -0,0 +1,97 @@ +-- TODO: create smods attributes: + -- xmult + -- xchips + -- retriggers + -- scaling + -- generation + -- spades + -- clubs + -- hearts + -- diamonds + -- hand_type + -- rank + -- copying + -- generate + -- food + -- space + -- discard + -- economy + +SMODS.Attributes = {} +SMODS.Attribute = SMODS.GameObject:extend { + obj_table = SMODS.Attributes, + set = 'Attribute', + obj_buffer = {}, + required_params = { + 'key', + }, + prefix_config = { key = false }, + process_loc_text = function() end, + inject = function(self) + self.key = string.lower(self.key) + self.keys = self.keys or {} + end, + post_inject_class = function(self) + for _, attribute in pairs(SMODS.Attributes) do + if attribute.alias then + for _, alias in ipairs(attribute.alias) do + if SMODS.Attributes[alias] then + SMODS.Attributes[alias].alias = SMODS.merge_lists({SMODS.Attributes[alias].alias or {}, {attribute.key}}) + end + end + end + end + end +} + +function SMODS.get_attribute_pool(attribute, seen) + local att = SMODS.Attributes[attribute] or {} + local out = att.keys or {} + seen = seen or {} + if not seen[attribute] and att.alias then + seen[attribute] = true + for _, alias in ipairs(att.alias) do + out = SMODS.merge_lists({out, SMODS.get_attribute_pool(alias, seen)}) + end + end + return out +end + +function SMODS.add_attribute(attribute_key, object_keys) + assert(SMODS.Attributes[attribute_key], "SMODS.add_attribute called with invaled attribute_key."..SMODS.log_crash_info(debug.getinfo(2))) + SMODS.Attributes[attribute_key].keys = SMODS.merge_lists({SMODS.Attributes[attribute_key].keys, object_keys}) +end + +function SMODS.populate_attributes() + for _, attribute in pairs(SMODS.Attributes) do + for _, key in ipairs(attribute.keys) do + if G.P_CENTERS[key] then + G.P_CENTERS[key].attributes = SMODS.merge_lists({G.P_CENTERS[key].attributes or {}, {attribute.key}}) + end + end + end +end + +SMODS.Attribute({ + key = 'mult', + keys = { + 'j_joker', 'j_greedy_joker', 'j_lusty_joker', 'j_wrathful_joker', 'j_gluttenous_joker', + 'j_jolly', 'j_zany', 'j_crazy', 'j_mad', 'j_droll', + 'j_half', 'j_ceremonial', 'j_mystic_summit', 'j_misprint', 'j_raised_fist', + 'j_fibonacci', 'j_abstract', 'j_gros_michel', 'j_even_steven', 'j_scholar', + 'j_ride_the_bus', 'j_green_joker', 'j_red_card', 'j_erosion', 'j_fortune_teller', + 'j_flash', 'j_popcorn', 'j_trousers', 'j_walkie_talkie', 'j_smiley', + 'j_swashbuckler', 'j_onyx_agate', 'j_shoot_the_moon', 'j_bootstraps', 'c_eris' + } +}) + +SMODS.Attribute({ + key = 'chips', + keys = { + 'j_sly', 'j_wily', 'j_clever', 'j_devious', 'j_crafty', + 'j_banner', 'j_scary_face', 'j_odd_todd', 'j_scholar', 'j_runner', + 'j_ice_cream', 'j_blue_joker', 'j_hiker', 'j_square', 'j_stone', + 'j_bull', 'j_walkie_talkie', 'j_castle', 'j_arrowhead', 'j_wee', + 'j_stuntman', 'c_eris' + } +}) \ No newline at end of file diff --git a/src/preflight/loader.lua b/src/preflight/loader.lua index 33d336a57..20355118c 100644 --- a/src/preflight/loader.lua +++ b/src/preflight/loader.lua @@ -818,6 +818,7 @@ local function initSteamodded() -- boot_print_stage("Injecting Items") SMODS.injectItems() convert_save_data() + SMODS.populate_attributes() SMODS.booted = true diff --git a/src/utils.lua b/src/utils.lua index be5a82904..3f22b1915 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -678,8 +678,8 @@ function SMODS.intersect_lists(lists) local function find_intersects(l1, l2) local seen = {} local ret = {} - for _, v in ipairs(l1) do seen[v] = true end - for _, v in ipairs(l2) do if seen[v] then ret[#ret + 1] = v end end + for _, v in ipairs(l1) do seen[v] = (seen[v] or 0) + 1 end + for _, v in ipairs(l2) do if seen[v] and seen[v] > 0 then ret[#ret + 1] = v; seen[v] = seen[v] - 1; print(v, seen[v]) end end return ret end From 2b68dc31767190605ab2bbc487e49dc2a5cb3c10 Mon Sep 17 00:00:00 2001 From: Eremel Date: Sun, 29 Mar 2026 21:10:01 +0100 Subject: [PATCH 12/32] update todo --- src/utils/weights.lua | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/utils/weights.lua b/src/utils/weights.lua index 74778d4df..83d980652 100644 --- a/src/utils/weights.lua +++ b/src/utils/weights.lua @@ -1,7 +1,4 @@ --- TODO: object labels SMODS class --- TODO: populate vanilla object labels (chips, mult, xmult, food, space, generation, diamond, heart, spade, club) --- TODO: object labels integrated into `get_current_pool` - +-- TODO: how do soul objects fit into this system? -- Returns a `key` of the polled object type ---@param args table|{type: string?, attributes: table[string]?, pool: table[string]?, seed: string?, chance: number?, guaranteed: boolean?} From f6c8d543b9bd35bba2b40210cf5c907959b5eb29 Mon Sep 17 00:00:00 2001 From: Eremel Date: Sun, 29 Mar 2026 22:13:51 +0100 Subject: [PATCH 13/32] remove print --- src/utils.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.lua b/src/utils.lua index 3f22b1915..a0a6f9223 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -679,7 +679,7 @@ function SMODS.intersect_lists(lists) local seen = {} local ret = {} for _, v in ipairs(l1) do seen[v] = (seen[v] or 0) + 1 end - for _, v in ipairs(l2) do if seen[v] and seen[v] > 0 then ret[#ret + 1] = v; seen[v] = seen[v] - 1; print(v, seen[v]) end end + for _, v in ipairs(l2) do if seen[v] and seen[v] > 0 then ret[#ret + 1] = v; seen[v] = seen[v] - 1 end end return ret end From 55ec34c23a68db8918cdaedd3858d74f25a3e074 Mon Sep 17 00:00:00 2001 From: Eremel Date: Mon, 30 Mar 2026 00:05:01 +0100 Subject: [PATCH 14/32] Add more attributes --- src/game_objects/attributes.lua | 189 ++++++++++++++++++++++++++++---- 1 file changed, 169 insertions(+), 20 deletions(-) diff --git a/src/game_objects/attributes.lua b/src/game_objects/attributes.lua index 012464adb..1ce9475c1 100644 --- a/src/game_objects/attributes.lua +++ b/src/game_objects/attributes.lua @@ -1,22 +1,3 @@ --- TODO: create smods attributes: - -- xmult - -- xchips - -- retriggers - -- scaling - -- generation - -- spades - -- clubs - -- hearts - -- diamonds - -- hand_type - -- rank - -- copying - -- generate - -- food - -- space - -- discard - -- economy - SMODS.Attributes = {} SMODS.Attribute = SMODS.GameObject:extend { obj_table = SMODS.Attributes, @@ -78,7 +59,7 @@ SMODS.Attribute({ 'j_joker', 'j_greedy_joker', 'j_lusty_joker', 'j_wrathful_joker', 'j_gluttenous_joker', 'j_jolly', 'j_zany', 'j_crazy', 'j_mad', 'j_droll', 'j_half', 'j_ceremonial', 'j_mystic_summit', 'j_misprint', 'j_raised_fist', - 'j_fibonacci', 'j_abstract', 'j_gros_michel', 'j_even_steven', 'j_scholar', + 'j_fibonacci', 'j_abstract', 'j_gros_michel', 'j_even_steven', 'j_scholar', 'j_supernova', 'j_ride_the_bus', 'j_green_joker', 'j_red_card', 'j_erosion', 'j_fortune_teller', 'j_flash', 'j_popcorn', 'j_trousers', 'j_walkie_talkie', 'j_smiley', 'j_swashbuckler', 'j_onyx_agate', 'j_shoot_the_moon', 'j_bootstraps', 'c_eris' @@ -94,4 +75,172 @@ SMODS.Attribute({ 'j_bull', 'j_walkie_talkie', 'j_castle', 'j_arrowhead', 'j_wee', 'j_stuntman', 'c_eris' } +}) + +SMODS.Attribute({ + key = 'xmult', + keys = { + 'j_stencil', 'j_loyalty_card', 'j_steel_joker', 'j_blackboard', 'j_constellation', + 'j_cavendish', 'j_card_sharp', 'j_madness', 'j_vampire', 'j_hologram', + 'j_baron', 'j_obelisk', 'j_photograph', 'j_lucky_cat', 'j_baseball', + 'j_ancient', 'j_ramen', 'j_campfire', 'j_acrobat', 'j_throwback', + 'j_bloodstone', 'j_glass', 'j_flower_pot', 'j_idol', 'j_seeing_double', + 'j_hit_the_road', 'j_duo', 'j_trio', 'j_family', 'j_order', 'j_tribe', + 'j_drivers_license', 'j_caino', 'j_triboulet', 'j_yorick' + } +}) + +SMODS.Attribute({ + key = 'xchips', +}) + +SMODS.Attribute({ + key = 'score', +}) + +SMODS.Attribute({ + key = 'xscore', +}) + +SMODS.Attribute({ + key = 'retrigger', + keys = { + 'j_mime', 'j_dusk', 'j_hack', 'j_selzer', 'j_sock_and_buskin', 'j_hanging_chad' + } +}) + +SMODS.Attribute({ + key = 'scaling', + keys = { + 'j_ceremonial', 'j_steel', 'j_ride_the_bus', 'j_egg', 'j_runner', + 'j_ice_cream', 'j_constellation', 'j_hiker', 'j_green_joker', 'j_red_card', + 'j_madness', 'j_square', 'j_vampire', 'j_hologram', 'j_rocket', + 'j_obelisk', 'j_gift', 'j_fortune_teller', 'j_stone', 'j_flash', + 'j_popcorn', 'j_trousers', 'j_ramen', 'j_castle', 'j_campfire', + 'j_throwback', 'j_glass', 'j_wee', 'j_hit_the_road', 'j_caino', 'j_yorick' + } +}) + +SMODS.Attribute({ + key = 'generation', + keys = { + 'j_marble', 'j_8_ball', 'j_dna', 'j_sixth_sense', 'j_superposition', + 'j_seance', 'j_riff_raff', 'j_vagabond', 'j_hallucination', 'j_diet_cola', + 'j_certificate', 'j_invisible', 'j_cartomancer', 'j_perkeo' + } +}) + +SMODS.Attribute({ + key = 'suit', + keys = { + 'j_greedy_joker', 'j_lusty_joker', 'j_wrathful_joker', 'j_gluttenous_joker', + 'j_smeared', 'j_castle', 'j_ancient', 'j_seeing_double', 'j_blackboard', 'j_flower_pot', + 'j_rough_gem', 'j_bloodstone', 'j_arrowhead', 'j_onyx_agate', + } +}) + +SMODS.Attribute({ + key = 'diamonds', + keys = { + 'j_greedy_joker', 'j_smeared', 'j_rough_gem' + } +}) + +SMODS.Attribute({ + key = 'hearts', + keys = { + 'j_lusty_joker', 'j_smeared', 'j_bloodstone' + } +}) + +SMODS.Attribute({ + key = 'spades', + keys = { + 'j_wrathful_joker', 'j_smeared', 'j_arrowhead', 'j_blackboard' + } +}) + +SMODS.Attribute({ + key = 'clubs', + keys = { + 'j_gluttenous_joker', 'j_smeared', 'j_onyx_agate', 'j_seeing_double', 'j_blackboard' + } +}) + +SMODS.Attribute({ + key = 'hand_type', + keys = { + 'j_jolly', 'j_zany', 'j_crazy', 'j_mad', 'j_droll', + 'j_sly', 'j_wily', 'j_clever', 'j_devious', 'j_crafty', + 'j_four_fingers', 'j_supernova', 'j_runner', 'j_superposition', + 'j_todo_list', 'j_seance', 'j_shortcut', 'j_obelisk', 'j_trousers', + 'j_duo', 'j_trio', 'j_family', 'j_order', 'j_tribe', + } +}) + +SMODS.Attribute({ + key = 'rank', + keys = { + 'j_8_ball', 'j_raised_fist', 'j_fibonacci', 'j_hack', 'j_even_steven', + 'j_odd_todd', 'j_scholar', 'j_sixth_sense', 'j_superposition', 'j_cloud_9', + 'j_mail', 'j_walkie_talkie', 'j_wee', 'j_idol', 'j_hit_the_road', + 'j_shoot_the_moon', 'j_triboulet' + } +}) + +SMODS.Attribute({ + key = 'face', + keys = { + 'j_scary', 'j_paraeidolia', 'j_business', 'j_ride_the_bus', + 'j_faceless', 'j_midas_mask', 'j_photograph', 'j_reserved_parking', + 'j_smiley', 'j_sock_and_buskin', 'j_caino' + } +}) + +SMODS.Attribute({ + key = 'copying', + keys = { + 'j_blueprint', 'j_brainstorm', + } +}) + +SMODS.Attribute({ + key = 'food', + keys = { + 'j_gros_michel', 'j_cavendish', 'j_ice_cream', 'j_ramen', 'j_turtle_bean', 'j_popcorn', 'j_seltzer' + } +}) + +SMODS.Attribute({ + key = 'space', + keys = { + 'j_supernova', 'j_space', 'j_constellation', 'j_rocket', 'j_satellite', 'j_astronomer' + } +}) + +SMODS.Attribute({ + key = 'discard', + keys = { + 'j_banner', 'j_mystic_summit', 'j_delayed_grat', 'j_burglar', 'j_faceless', + 'j_green_joker', 'j_mail', 'j_drunkard', 'j_trading', 'j_ramen', 'j_castle', + 'j_merry_andy', 'j_hit_the_road', 'j_burnt', 'j_yorick' + } +}) + +SMODS.Attribute({ + key = 'economy', + keys = { + 'j_credit_card', 'j_chaos', 'j_delayed_grat', 'j_business', 'j_egg', + 'j_faceless', 'j_todo_list', 'j_cloud_9', 'j_rocket', 'j_gift', + 'j_reserved_parking', 'j_mail', 'j_to_the_moon', 'j_golden', + 'j_trading', 'j_ticket', 'j_rough_gem', 'j_matador', 'j_satellite' + } +}) + +SMODS.Attribute({ + key = 'chance', + keys = { + 'j_8_ball', 'j_gros_michel', 'j_business', 'j_space', 'j_cavendish', + 'j_hallucination', 'j_reserved_parking', 'j_bloodstone', + } }) \ No newline at end of file From b0676691e5a26741c1faf38d789a7594e1c61d89 Mon Sep 17 00:00:00 2001 From: Eremel Date: Mon, 30 Mar 2026 00:58:21 +0100 Subject: [PATCH 15/32] :) --- src/game_objects/attributes.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/game_objects/attributes.lua b/src/game_objects/attributes.lua index 1ce9475c1..e6050994f 100644 --- a/src/game_objects/attributes.lua +++ b/src/game_objects/attributes.lua @@ -62,7 +62,7 @@ SMODS.Attribute({ 'j_fibonacci', 'j_abstract', 'j_gros_michel', 'j_even_steven', 'j_scholar', 'j_supernova', 'j_ride_the_bus', 'j_green_joker', 'j_red_card', 'j_erosion', 'j_fortune_teller', 'j_flash', 'j_popcorn', 'j_trousers', 'j_walkie_talkie', 'j_smiley', - 'j_swashbuckler', 'j_onyx_agate', 'j_shoot_the_moon', 'j_bootstraps', 'c_eris' + 'j_swashbuckler', 'j_onyx_agate', 'j_shoot_the_moon', 'j_bootstraps' } }) @@ -73,7 +73,7 @@ SMODS.Attribute({ 'j_banner', 'j_scary_face', 'j_odd_todd', 'j_scholar', 'j_runner', 'j_ice_cream', 'j_blue_joker', 'j_hiker', 'j_square', 'j_stone', 'j_bull', 'j_walkie_talkie', 'j_castle', 'j_arrowhead', 'j_wee', - 'j_stuntman', 'c_eris' + 'j_stuntman' } }) From 33d23522d0ff3a47563b875b059bebc4d50f8ce7 Mon Sep 17 00:00:00 2001 From: Eremel Date: Mon, 30 Mar 2026 23:19:43 +0100 Subject: [PATCH 16/32] Change default behaviour of polling multiple attributes to intersect, added `args.union` to join attribute pools together instead --- src/utils/weights.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/weights.lua b/src/utils/weights.lua index 83d980652..9cff70ff1 100644 --- a/src/utils/weights.lua +++ b/src/utils/weights.lua @@ -233,7 +233,7 @@ function SMODS.create_poll_pool(labels, args) for _, label in ipairs(labels) do labels_used[label] = true local temp_pool = {} - local join_func = args.intersect and SMODS.intersect_lists or join_lists + local join_func = (args.attributes and not args.union) and SMODS.intersect_lists or join_lists for i=1, #(args.rarities or {true}) do local _p = label == 'Blind' and SMODS.create_blind_pool(args.blind_type or 'boss') or SMODS.Attributes[label] and SMODS.get_attribute_pool(label) or get_current_pool(label, args.rarities and args.rarities[i]) if SMODS.Attributes[label] then From 8e88af428e8a1da990dae098ca5d2b2a0e3a8f13 Mon Sep 17 00:00:00 2001 From: Eremel Date: Mon, 30 Mar 2026 23:35:24 +0100 Subject: [PATCH 17/32] Add `allow_legendaries` flag --- src/utils/weights.lua | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/src/utils/weights.lua b/src/utils/weights.lua index 9cff70ff1..d50f8f792 100644 --- a/src/utils/weights.lua +++ b/src/utils/weights.lua @@ -380,31 +380,32 @@ function SMODS.cull_pool(pool, type, args) for _, key in ipairs(pool) do local add = nil local v = G.P_CENTERS[key] - local in_pool, pool_opts = SMODS.add_to_pool(v, { source = args.append }) - pool_opts = pool_opts or {} - if not (G.GAME.used_jokers[v.key] and not pool_opts.allow_duplicates and not SMODS.showman(v.key) and not args.allow_duplicates) and - (v.unlocked ~= false or v.rarity == 4) then - if v.enhancement_gate then - add = nil - for kk, vv in pairs(G.playing_cards) do - if SMODS.has_enhancement(vv, v.enhancement_gate) then - add = true + if v then + local in_pool, pool_opts = SMODS.add_to_pool(v, { source = args.append }) + pool_opts = pool_opts or {} + if not (G.GAME.used_jokers[v.key] and not pool_opts.allow_duplicates and not SMODS.showman(v.key) and not args.allow_duplicates) and (v.unlocked ~= false or (v.rarity == 4 and args.allow_legendaries)) then + if v.enhancement_gate then + add = nil + for kk, vv in pairs(G.playing_cards) do + if SMODS.has_enhancement(vv, v.enhancement_gate) then + add = true + end end + else + add = true end - else - add = true end - end - 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 - - add = in_pool and (add or pool_opts.override_base_checks) - - if add and not G.GAME.banned_keys[v.key] then - final_pool[#final_pool + 1] = v.key - else - final_pool[#final_pool + 1] = 'UNAVAILABLE' + 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 + + add = in_pool and (add or pool_opts.override_base_checks) + + if add and not G.GAME.banned_keys[v.key] then + final_pool[#final_pool + 1] = v.key + else + final_pool[#final_pool + 1] = 'UNAVAILABLE' + end end end From 8bafe3953fe6105c835faebc64d83f5263c08bc7 Mon Sep 17 00:00:00 2001 From: Eremel Date: Tue, 31 Mar 2026 12:43:42 +0100 Subject: [PATCH 18/32] Attribute pollling respects rarity by default. Add `rarity` value when using attributes, can be a key or `false' which will ignore rarity weights --- src/utils/weights.lua | 71 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/src/utils/weights.lua b/src/utils/weights.lua index d50f8f792..2f226bcaf 100644 --- a/src/utils/weights.lua +++ b/src/utils/weights.lua @@ -237,7 +237,7 @@ function SMODS.create_poll_pool(labels, args) for i=1, #(args.rarities or {true}) do local _p = label == 'Blind' and SMODS.create_blind_pool(args.blind_type or 'boss') or SMODS.Attributes[label] and SMODS.get_attribute_pool(label) or get_current_pool(label, args.rarities and args.rarities[i]) if SMODS.Attributes[label] then - _p = SMODS.cull_pool(_p, label, args) + _p = SMODS.cull_pool(_p, args) end if label == 'Edition' then local _options = {} @@ -375,15 +375,69 @@ function SMODS.poll_object_type(args) return weighted_table[ind].type end -function SMODS.cull_pool(pool, type, args) +local function SMODS_WEIGHTS_poll_rarity(pool, args) + local rarity_poll = pseudorandom(pseudoseed((args.seed or 'smods_cull_rarity')..'_cull' )) -- Generate the poll value + local available_rarities = copy_table(SMODS.ObjectTypes[args.type or 'Joker'].rarities) -- Table containing a list of rarities and their rates + local vanilla_rarities = {["Common"] = 1, ["Uncommon"] = 2, ["Rare"] = 3, ["Legendary"] = 4} + + -- Check to see if any rarities are empty and should be disabled + for _, v in ipairs(available_rarities) do + local missing = true + local i = 1 + while missing and i <= #pool do + if G.P_CENTERS[pool[i]] and G.P_CENTERS[pool[i]].rarity == (vanilla_rarities[v.key] or v.key) then + missing = false + end + i = i+1 + end + if missing then + SMODS.remove_pool(available_rarities, v.key) + end + end + + -- Calculate total rates of rarities + local total_weight = 0 + for _, v in ipairs(available_rarities) do + v.mod = G.GAME[tostring(v.key):lower().."_mod"] or 1 + -- Should this fully override the v.weight calcs? + if SMODS.Rarities[v.key] and SMODS.Rarities[v.key].get_weight and type(SMODS.Rarities[v.key].get_weight) == "function" then + v.weight = SMODS.Rarities[v.key]:get_weight(v.weight, SMODS.ObjectTypes[args.type or 'Joker']) + end + v.weight = v.weight*v.mod + total_weight = total_weight + v.weight + end + -- recalculate rarities to account for v.mod + for _, v in ipairs(available_rarities) do + v.weight = v.weight / total_weight + end + + -- Calculate selected rarity + local weight_i = 0 + for _, v in ipairs(available_rarities) do + weight_i = weight_i + v.weight + if rarity_poll < weight_i then + if vanilla_rarities[v.key] then + return vanilla_rarities[v.key] + else + return v.key + end + end + end + return nil +end + +function SMODS.cull_pool(pool, args) local final_pool = {} + + local _rarity = args.rarity and args.rarity ~= true and args.rarity + for _, key in ipairs(pool) do local add = nil local v = G.P_CENTERS[key] if v then local in_pool, pool_opts = SMODS.add_to_pool(v, { source = args.append }) pool_opts = pool_opts or {} - if not (G.GAME.used_jokers[v.key] and not pool_opts.allow_duplicates and not SMODS.showman(v.key) and not args.allow_duplicates) and (v.unlocked ~= false or (v.rarity == 4 and args.allow_legendaries)) then + if not (G.GAME.used_jokers[v.key] and not pool_opts.allow_duplicates and not SMODS.showman(v.key) and not args.allow_duplicates) and (v.unlocked ~= false or (v.rarity == 4 and args.allow_legendaries)) and (not _rarity or _rarity == v.rarity) then if v.enhancement_gate then add = nil for kk, vv in pairs(G.playing_cards) do @@ -408,7 +462,16 @@ function SMODS.cull_pool(pool, type, args) end end end - + + if not _rarity and args.rarity ~= false then + _rarity = SMODS_WEIGHTS_poll_rarity(final_pool, args) + for _, k in ipairs(final_pool) do + if G.P_CENTERS[k] and G.P_CENTERS[k].rarity ~= _rarity then + final_pool[_] = 'UNAVAILABLE' + end + end + end + return final_pool end From c072750efd13e65ebef36fa5a152dd469b08eae3 Mon Sep 17 00:00:00 2001 From: Eremel Date: Tue, 31 Mar 2026 12:44:31 +0100 Subject: [PATCH 19/32] Add individual rank attributes --- src/game_objects/attributes.lua | 93 ++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/src/game_objects/attributes.lua b/src/game_objects/attributes.lua index e6050994f..8f88df54d 100644 --- a/src/game_objects/attributes.lua +++ b/src/game_objects/attributes.lua @@ -183,11 +183,102 @@ SMODS.Attribute({ keys = { 'j_8_ball', 'j_raised_fist', 'j_fibonacci', 'j_hack', 'j_even_steven', 'j_odd_todd', 'j_scholar', 'j_sixth_sense', 'j_superposition', 'j_cloud_9', - 'j_mail', 'j_walkie_talkie', 'j_wee', 'j_idol', 'j_hit_the_road', + 'j_mail', 'j_walkie_talkie', 'j_wee', 'j_idol', 'j_hit_the_road', 'j_baron', 'j_shoot_the_moon', 'j_triboulet' } }) +SMODS.Attribute({ + key = 'ace', + keys = { + 'j_fibonacci', 'j_odd_todd', 'j_scholar', 'j_superposition' + } +}) + +SMODS.Attribute({ + key = 'two', + keys = { + 'j_fibonacci', 'j_hack', 'j_even_steven', 'j_wee' + } +}) + +SMODS.Attribute({ + key = 'three', + keys = { + 'j_fibonacci', 'j_hack', 'j_odd_todd' + } +}) + +SMODS.Attribute({ + key = 'four', + keys = { + 'j_hack', 'j_even_steven', 'j_walkie_talkie' + } +}) + +SMODS.Attribute({ + key = 'five', + keys = { + 'j_fibonacci', 'j_hack', 'j_odd_todd' + } +}) + +SMODS.Attribute({ + key = 'six', + keys = { + 'j_even_steven', 'j_sixth_sense' + } +}) + +SMODS.Attribute({ + key = 'seven', + keys = { + 'j_odd_todd' + } +}) + +SMODS.Attribute({ + key = 'eight', + keys = { + 'j_8_ball', 'j_even_steven' + } +}) + +SMODS.Attribute({ + key = 'nine', + keys = { + 'j_odd_todd', 'j_cloud_9' + } +}) + +SMODS.Attribute({ + key = 'ten', + keys = { + 'j_even_steven', 'j_walkie_talkie' + } +}) + +SMODS.Attribute({ + key = 'jack', + keys = { + 'j_hit_the_road' + } +}) + +SMODS.Attribute({ + key = 'queen', + keys = { + 'j_shoot_the_moon', 'j_triboulet' + } +}) + +SMODS.Attribute({ + key = 'king', + keys = { + 'j_baron', 'j_triboulet' + } +}) + SMODS.Attribute({ key = 'face', keys = { From 2a96934b0e750bae21685646b60c346266f26bc2 Mon Sep 17 00:00:00 2001 From: Eremel Date: Tue, 31 Mar 2026 13:50:43 +0100 Subject: [PATCH 20/32] Add support for attributes in `SMODS.create_card`, slight rarity fix --- src/utils.lua | 3 ++ src/utils/weights.lua | 117 ++++++++++++++++++++---------------------- 2 files changed, 60 insertions(+), 60 deletions(-) diff --git a/src/utils.lua b/src/utils.lua index 84c4baddd..1491da2b6 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -372,6 +372,9 @@ function SMODS.find_card(key, count_debuffed) end function SMODS.create_card(t) + if not t.key and t.attributes then + t.key = SMODS.poll_object(t) + end if not t.area and t.key and G.P_CENTERS[t.key] then t.area = G.P_CENTERS[t.key].consumeable and G.consumeables or G.P_CENTERS[t.key].set == 'Joker' and G.jokers end diff --git a/src/utils/weights.lua b/src/utils/weights.lua index 2f226bcaf..fcc88b9b3 100644 --- a/src/utils/weights.lua +++ b/src/utils/weights.lua @@ -214,6 +214,57 @@ function SMODS.create_blind_pool(blind_type, skip_cull) return output end +local function SMODS_WEIGHTS_poll_rarity(pool, args) + local rarity_poll = pseudorandom(pseudoseed((args.seed or 'smods_cull_rarity')..'_cull' )) -- Generate the poll value + local available_rarities = copy_table(SMODS.ObjectTypes[args.type or 'Joker'].rarities) -- Table containing a list of rarities and their rates + local vanilla_rarities = {["Common"] = 1, ["Uncommon"] = 2, ["Rare"] = 3, ["Legendary"] = 4} + local final_rarities = {} + -- Check to see if any rarities are empty and should be disabled + for _, v in ipairs(available_rarities) do + local missing = true + local i = 1 + while missing and i <= #pool do + if G.P_CENTERS[pool[i]] and G.P_CENTERS[pool[i]].rarity == (vanilla_rarities[v.key] or v.key) then + missing = false + end + i = i+1 + end + if not missing then + final_rarities[#final_rarities + 1] = v + end + end + + -- Calculate total rates of rarities + local total_weight = 0 + for _, v in ipairs(final_rarities) do + v.mod = G.GAME[tostring(v.key):lower().."_mod"] or 1 + -- Should this fully override the v.weight calcs? + if SMODS.Rarities[v.key] and SMODS.Rarities[v.key].get_weight and type(SMODS.Rarities[v.key].get_weight) == "function" then + v.weight = SMODS.Rarities[v.key]:get_weight(v.weight, SMODS.ObjectTypes[args.type or 'Joker']) + end + v.weight = v.weight*v.mod + total_weight = total_weight + v.weight + end + -- recalculate rarities to account for v.mod + for _, v in ipairs(final_rarities) do + v.weight = v.weight / total_weight + end + + -- Calculate selected rarity + local weight_i = 0 + for _, v in ipairs(final_rarities) do + weight_i = weight_i + v.weight + if rarity_poll < weight_i then + if vanilla_rarities[v.key] then + return vanilla_rarities[v.key] + else + return v.key + end + end + end + return nil +end + -- Create a table of {key = string, type = label} items to be polled function SMODS.create_poll_pool(labels, args) local labels_used = {} @@ -229,7 +280,7 @@ function SMODS.create_poll_pool(labels, args) end return l1 end - + for _, label in ipairs(labels) do labels_used[label] = true local temp_pool = {} @@ -257,6 +308,11 @@ function SMODS.create_poll_pool(labels, args) end final_pool = final_pool and join_func({final_pool, temp_pool}) or temp_pool end + + if args.attributes and not args.rarity and args.rarity ~= false then + args.rarity = SMODS_WEIGHTS_poll_rarity(final_pool, args) + final_pool = SMODS.cull_pool(final_pool, args) + end local ret_pool = {} local pool_exists = false @@ -375,57 +431,6 @@ function SMODS.poll_object_type(args) return weighted_table[ind].type end -local function SMODS_WEIGHTS_poll_rarity(pool, args) - local rarity_poll = pseudorandom(pseudoseed((args.seed or 'smods_cull_rarity')..'_cull' )) -- Generate the poll value - local available_rarities = copy_table(SMODS.ObjectTypes[args.type or 'Joker'].rarities) -- Table containing a list of rarities and their rates - local vanilla_rarities = {["Common"] = 1, ["Uncommon"] = 2, ["Rare"] = 3, ["Legendary"] = 4} - - -- Check to see if any rarities are empty and should be disabled - for _, v in ipairs(available_rarities) do - local missing = true - local i = 1 - while missing and i <= #pool do - if G.P_CENTERS[pool[i]] and G.P_CENTERS[pool[i]].rarity == (vanilla_rarities[v.key] or v.key) then - missing = false - end - i = i+1 - end - if missing then - SMODS.remove_pool(available_rarities, v.key) - end - end - - -- Calculate total rates of rarities - local total_weight = 0 - for _, v in ipairs(available_rarities) do - v.mod = G.GAME[tostring(v.key):lower().."_mod"] or 1 - -- Should this fully override the v.weight calcs? - if SMODS.Rarities[v.key] and SMODS.Rarities[v.key].get_weight and type(SMODS.Rarities[v.key].get_weight) == "function" then - v.weight = SMODS.Rarities[v.key]:get_weight(v.weight, SMODS.ObjectTypes[args.type or 'Joker']) - end - v.weight = v.weight*v.mod - total_weight = total_weight + v.weight - end - -- recalculate rarities to account for v.mod - for _, v in ipairs(available_rarities) do - v.weight = v.weight / total_weight - end - - -- Calculate selected rarity - local weight_i = 0 - for _, v in ipairs(available_rarities) do - weight_i = weight_i + v.weight - if rarity_poll < weight_i then - if vanilla_rarities[v.key] then - return vanilla_rarities[v.key] - else - return v.key - end - end - end - return nil -end - function SMODS.cull_pool(pool, args) local final_pool = {} @@ -463,14 +468,6 @@ function SMODS.cull_pool(pool, args) end end - if not _rarity and args.rarity ~= false then - _rarity = SMODS_WEIGHTS_poll_rarity(final_pool, args) - for _, k in ipairs(final_pool) do - if G.P_CENTERS[k] and G.P_CENTERS[k].rarity ~= _rarity then - final_pool[_] = 'UNAVAILABLE' - end - end - end return final_pool end From 97abc5f100147ca925063130d99612a78def3992 Mon Sep 17 00:00:00 2001 From: Eremel Date: Tue, 31 Mar 2026 13:52:52 +0100 Subject: [PATCH 21/32] Add mod_chance attribute --- src/game_objects/attributes.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/game_objects/attributes.lua b/src/game_objects/attributes.lua index 8f88df54d..0a386b9f7 100644 --- a/src/game_objects/attributes.lua +++ b/src/game_objects/attributes.lua @@ -334,4 +334,11 @@ SMODS.Attribute({ 'j_8_ball', 'j_gros_michel', 'j_business', 'j_space', 'j_cavendish', 'j_hallucination', 'j_reserved_parking', 'j_bloodstone', } +}) + +SMODS.Attribute({ + key = 'mod_chance', + keys = { + 'j_oops' + } }) \ No newline at end of file From 6f83b746f3f8faa6b58287b1793601061d987d42 Mon Sep 17 00:00:00 2001 From: Eremel Date: Tue, 31 Mar 2026 14:24:27 +0100 Subject: [PATCH 22/32] Attempting to register the same attribute key will now merge the keys tables --- src/game_object.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/game_object.lua b/src/game_object.lua index 662e70170..36a79016b 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -115,6 +115,10 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. -- Checked on __call but not take_ownership. For take_ownership, the key must exist function SMODS.GameObject:check_duplicate_key() if self.obj_table[self.key] or (self.get_obj and self:get_obj(self.key)) then + if self.set == 'Attribute' then + SMODS.Attributes[self.key].keys = SMODS.merge_lists({SMODS.Attributes[self.key].keys, self.keys}) + return true + end sendWarnMessage(('Object %s has the same key as an existing object, not registering.'):format(self.key), self.set) sendWarnMessage('If you want to modify an existing object, use take_ownership()', self.set) return true From 44ef334c2d283e7283ec8cd36656d4acc659a749 Mon Sep 17 00:00:00 2001 From: Eremel Date: Tue, 31 Mar 2026 14:44:32 +0100 Subject: [PATCH 23/32] Add `closest_match` flag to ensure attributes don't reduce the pool to empty --- src/utils/weights.lua | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/utils/weights.lua b/src/utils/weights.lua index fcc88b9b3..b9d9389d0 100644 --- a/src/utils/weights.lua +++ b/src/utils/weights.lua @@ -269,7 +269,7 @@ end function SMODS.create_poll_pool(labels, args) local labels_used = {} local pool = {} - local final_pool + local final_pool = {} local it = 0 local function join_lists(args) @@ -280,6 +280,13 @@ function SMODS.create_poll_pool(labels, args) end return l1 end + + local function pool_exists(pool) + for _, v in ipairs(pool) do + if v ~= 'UNAVAILABLE' then return true end + end + return false + end for _, label in ipairs(labels) do labels_used[label] = true @@ -306,7 +313,8 @@ function SMODS.create_poll_pool(labels, args) for _, v in ipairs(temp_pool) do pool[v] = {key = v, type = label} end - final_pool = final_pool and join_func({final_pool, temp_pool}) or temp_pool + local temp = pool_exists(final_pool) and final_pool and join_func({final_pool, temp_pool}) or temp_pool + final_pool = (args.closest_match and not pool_exists(temp)) and final_pool or temp end if args.attributes and not args.rarity and args.rarity ~= false then From a3166d66a784cfa8f70e9e6acbaaeb06302df95f Mon Sep 17 00:00:00 2001 From: Eremel Date: Tue, 31 Mar 2026 20:32:15 +0100 Subject: [PATCH 24/32] Add consumable type attributes --- src/game_objects/attributes.lua | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/game_objects/attributes.lua b/src/game_objects/attributes.lua index 0a386b9f7..01aeb5244 100644 --- a/src/game_objects/attributes.lua +++ b/src/game_objects/attributes.lua @@ -282,7 +282,7 @@ SMODS.Attribute({ SMODS.Attribute({ key = 'face', keys = { - 'j_scary', 'j_paraeidolia', 'j_business', 'j_ride_the_bus', + 'j_scary', 'j_pareidolia', 'j_business', 'j_ride_the_bus', 'j_faceless', 'j_midas_mask', 'j_photograph', 'j_reserved_parking', 'j_smiley', 'j_sock_and_buskin', 'j_caino' } @@ -341,4 +341,25 @@ SMODS.Attribute({ keys = { 'j_oops' } +}) + +SMODS.Attribute({ + key = 'tarot', + keys = { + 'j_8_ball', 'j_superposition', 'j_vagabond', 'j_hallucination', 'j_fortune_teller', 'j_cartomancer' + } +}) + +SMODS.Attribute({ + key = 'planet', + keys = { + 'j_astronomer', 'j_constellation', 'j_satellite' + } +}) + +SMODS.Attribute({ + key = 'spectral', + keys = { + 'j_sixth_sense', 'j_seance' + } }) \ No newline at end of file From 6605ba780d6fe9b7869a34e678cb89854fc6f5c9 Mon Sep 17 00:00:00 2001 From: Eremel Date: Tue, 31 Mar 2026 20:47:38 +0100 Subject: [PATCH 25/32] Clean up --- lovely/weights.toml | 81 ++++++++++++++++++------------------------- src/overrides.lua | 4 ++- src/utils.lua | 7 ++-- src/utils/weights.lua | 27 ++++++--------- 4 files changed, 51 insertions(+), 68 deletions(-) diff --git a/lovely/weights.toml b/lovely/weights.toml index 3ae783339..d2e1f1b18 100644 --- a/lovely/weights.toml +++ b/lovely/weights.toml @@ -3,25 +3,51 @@ version = "1.0.0" dump_lua = true priority = -10 +# Bypass get_new_boss() +[[patches]] +[patches.pattern] +target = 'functions/common_events.lua' +match_indent = true +position = 'after' +pattern = ''' +function get_new_boss() +''' +payload = ''' +-- Use SMODS object weight system when enabled +if SMODS.optional_features.object_weights then return SMODS.poll_object({type = 'Blind'}) end +''' -# Escape get_new_boss early for SMODS.poll_object +# Bypass create_card_for_shop() +[[patches]] +[patches.pattern] +target = 'functions/UI_definitions.lua' +match_indent = true +position = 'after' +pattern = ''' +function create_card_for_shop(area) +''' +payload = ''' +-- Use SMODS object weight system when enabled +if SMODS.optional_features.object_weights then return SMODS.create_shop_card(area) end +''' + +# Bypass create_card key selection [[patches]] [patches.pattern] target = 'functions/common_events.lua' match_indent = true position = 'before' pattern = ''' -local _, boss = pseudorandom_element(eligible_bosses, pseudoseed('boss')) +if forced_key and not G.GAME.banned_keys[forced_key] then ''' payload = ''' -if early_return then - local boss_keys = {} - for k, _ in pairs(eligible_bosses) do table.insert(boss_keys, k) end - return boss_keys -end +-- Use SMODS object weight system when enabled +if not forced_key and SMODS.optional_features.object_weights then forced_key = SMODS.poll_object({type = _type, guaranteed = true}) end ''' +# These patches add functionality for polling blinds, but are a precursor for full support of modded small and big blinds +# TODO: move to blind related toml # Adjust vanilla blinds max ante property [[patches]] [patches.pattern] @@ -55,45 +81,4 @@ if v.boss then bosses_used[k] = 0 end ''' payload = ''' if v.small or v.big then bosses_used[k] = 0 end -''' - -# Bypass get_new_boss() -[[patches]] -[patches.pattern] -target = 'functions/common_events.lua' -match_indent = true -position = 'after' -pattern = ''' -function get_new_boss() -''' -payload = ''' -if SMODS.optional_features.object_weights then return SMODS.poll_object({type = 'Blind'}) end -''' - -# Bypass create_card_for_shop() -[[patches]] -[patches.pattern] -target = 'functions/UI_definitions.lua' -match_indent = true -position = 'after' -pattern = ''' -function create_card_for_shop(area) -''' -payload = ''' -if SMODS.optional_features.object_weights then return SMODS.create_shop_card(area) end -''' - -# Bypass create_card key selection -[[patches]] -[patches.pattern] -target = 'functions/common_events.lua' -match_indent = true -position = 'before' -pattern = ''' -if forced_key and not G.GAME.banned_keys[forced_key] then -''' -payload = ''' --- Use SMODS object weight system when enabled -if not forced_key and SMODS.optional_features.object_weights then forced_key = SMODS.poll_object({type = _type, guaranteed = true}) end - ''' \ No newline at end of file diff --git a/src/overrides.lua b/src/overrides.lua index e464aa66d..633da9e1f 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -2227,7 +2227,7 @@ function poll_edition(_key, _mod, _no_neg, _guaranteed, _options) _options = { 'e_negative', 'e_polychrome', 'e_holo', 'e_foil' } end - -- BYPASS REST OF FUNCTION WHEN WEIGHTS BEING USED + -- Use SMODS object weight system when enabled if SMODS.optional_features.object_weights then return SMODS.poll_object({type = 'Edition', seed = _key, guaranteed = _guaranteed, pool = _options, no_negative = _no_neg, mod = _mod}) end @@ -2446,6 +2446,8 @@ function get_pack(_key, _type) G.GAME.first_shop_buffoon = true return G.P_CENTERS['p_buffoon_normal_'..(math.random(1, 2))] end + + -- Use SMODS object weight system when enabled if SMODS.optional_features.object_weights then return G.P_CENTERS[SMODS.poll_object({type = 'Booster'})] end diff --git a/src/utils.lua b/src/utils.lua index 1491da2b6..eb21c815b 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -372,6 +372,7 @@ function SMODS.find_card(key, count_debuffed) end function SMODS.create_card(t) + -- Support SMODS.Attributes if not t.key and t.attributes then t.key = SMODS.poll_object(t) end @@ -720,7 +721,7 @@ function SMODS.poll_edition(args) end function SMODS.poll_seal(args) - -- BYPASS REST OF FUNCTION WHEN WEIGHTS BEING USED + -- Use SMODS object weight system when enabled if SMODS.optional_features.object_weights then args.type = 'Seal'; return SMODS.poll_object(args) end args = args or {} @@ -738,7 +739,7 @@ function SMODS.poll_seal(args) local seal_option = {} if type(v) == 'string' then assert(G.P_SEALS[v], ("Could not find seal \"%s\"."):format(v)) - seal_option = { key = v, weight = G.P_SEALS[v].weight or 10 } -- default weight set to 5 to replicate base game weighting + seal_option = { key = v, weight = G.P_SEALS[v].weight or 10 } -- default weight set to 10 to respect SMODS weight system elseif type(v) == 'table' then assert(G.P_SEALS[v.key], ("Could not find seal \"%s\"."):format(v.key)) seal_option = { key = v.key, weight = v.weight } @@ -2442,6 +2443,8 @@ function SMODS.get_next_vouchers(vouchers) local _pool, _pool_key = get_current_pool('Voucher') for i=#vouchers+1, math.min(SMODS.size_of_pool(_pool), G.GAME.starting_params.vouchers_in_shop + (G.GAME.modifiers.extra_vouchers or 0)) do local center + + -- Use SMODS object weight system when enabled if SMODS.optional_features.object_weights then center = SMODS.poll_object({type = 'Voucher'}) else diff --git a/src/utils/weights.lua b/src/utils/weights.lua index b9d9389d0..03df928c4 100644 --- a/src/utils/weights.lua +++ b/src/utils/weights.lua @@ -3,21 +3,12 @@ -- Returns a `key` of the polled object type ---@param args table|{type: string?, attributes: table[string]?, pool: table[string]?, seed: string?, chance: number?, guaranteed: boolean?} function SMODS.poll_object(args) - assert(SMODS.optional_features.object_weights, "SMODS.poll_object called with object_weights optional feature disabled."..SMODS.log_crash_info(debug.getinfo(2))) assert(args, "SMODS.poll_object called with no args."..SMODS.log_crash_info(debug.getinfo(2))) assert((args.type or (args.types and type(args.types) == 'table') or (args.attributes and type(args.attributes) == 'table') or (args.pool and type(args.pool) == 'table')), "SMODS.poll_object called without a pool source." .. SMODS.log_crash_info(debug.getinfo(2))) - -- TODO: remove before merge - local function table_as_string(t) - local str = '' - for _, v in ipairs(t) do str = str .. v .. ', ' end - return str - end - -- Prepare pool local pool = args.pool or {} local types = args.attributes or args.types or {args.type} - if SMODS.debug_prints then print('Polling', table_as_string(types)) end -- Populate pool local types_used = {} @@ -62,8 +53,9 @@ function SMODS.poll_object(args) local chance = args.guaranteed and 1 or ((args.chance or SMODS.base_rate_percentage[args.type] or 1) * (args.mod or 1) * (modded_weight/total_weight)) -- Adjust chance based on modified weightings -- chance = chance * (modded_weight/total_weight) - local key = 'UNAVAILABLE' - while key == 'UNAVAILABLE' do + local output_key = 'UNAVAILABLE' + + while output_key == 'UNAVAILABLE' do local poll_key = pseudorandom(pseudoseed(args.seed or SMODS.get_poll_key(args.type))) if args.print then print('Total Weight: '..total_weight) end @@ -75,7 +67,6 @@ function SMODS.poll_object(args) if poll_key < (1 - chance) then if args.print then print('Poll failed') end - if SMODS.debug_prints then print("Result: none") end return end @@ -91,16 +82,17 @@ function SMODS.poll_object(args) if args.print then print('Looking for item: '..poll_weight) end if poll_weight > final_pool[1].mod_weight then - key = final_pool[SMODS.select_by_weight(final_pool, poll_weight, 1, #final_pool)].key + output_key = final_pool[SMODS.select_by_weight(final_pool, poll_weight, 1, #final_pool)].key else - key = final_pool[1].key + output_key = final_pool[1].key end end -- Edition specific functionality - if args.no_negative and key == 'e_negative' then return 'e_polychrome' end - if SMODS.debug_prints then print("Result: "..key) end - return key + if args.no_negative and output_key == 'e_negative' then return 'e_polychrome' end + + + return output_key end -- Returns the `weight` and `modified_weight` or a given object @@ -219,6 +211,7 @@ local function SMODS_WEIGHTS_poll_rarity(pool, args) local available_rarities = copy_table(SMODS.ObjectTypes[args.type or 'Joker'].rarities) -- Table containing a list of rarities and their rates local vanilla_rarities = {["Common"] = 1, ["Uncommon"] = 2, ["Rare"] = 3, ["Legendary"] = 4} local final_rarities = {} + -- Check to see if any rarities are empty and should be disabled for _, v in ipairs(available_rarities) do local missing = true From cce86520e49c06bbc1a0ccb2cccc18ecbacbffd2 Mon Sep 17 00:00:00 2001 From: Eremel Date: Tue, 31 Mar 2026 20:51:19 +0100 Subject: [PATCH 26/32] missed a debug print --- src/utils/weights.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/weights.lua b/src/utils/weights.lua index 03df928c4..09b8edd49 100644 --- a/src/utils/weights.lua +++ b/src/utils/weights.lua @@ -391,7 +391,6 @@ function SMODS.create_shop_card(area) end function SMODS.poll_object_type(args) - if SMODS.debug_prints then print "Using SMODS.poll_object_type" end args = args or {} -- If no types are given to select between, populate the list with all valid types From 596b6a087a18a2b5b320843be6ddc57bf0204985 Mon Sep 17 00:00:00 2001 From: Eremel Date: Tue, 31 Mar 2026 23:20:50 +0100 Subject: [PATCH 27/32] Fix `get_pack` ignoring `_type` --- src/overrides.lua | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/overrides.lua b/src/overrides.lua index 633da9e1f..8695df9a6 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -2449,7 +2449,16 @@ function get_pack(_key, _type) -- Use SMODS object weight system when enabled if SMODS.optional_features.object_weights then - return G.P_CENTERS[SMODS.poll_object({type = 'Booster'})] + return G.P_CENTERS[SMODS.poll_object({type = 'Booster', + filter = _type and function(pool) + local out = {} + for _, v in ipairs(pool) do + if G.P_CENTERS[v.key].kind == _type then + out[#out + 1] = v + end + end + return out + end})] end local cume, it, center = 0, 0, nil local temp_in_pool = {} From 80fefc8a13e3573c917f706a04c27347366ddf99 Mon Sep 17 00:00:00 2001 From: Eremel Date: Tue, 31 Mar 2026 23:34:25 +0100 Subject: [PATCH 28/32] Fix chance roll being adjusted when it shouldn't --- src/utils/weights.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/weights.lua b/src/utils/weights.lua index 09b8edd49..88378ed43 100644 --- a/src/utils/weights.lua +++ b/src/utils/weights.lua @@ -50,7 +50,7 @@ function SMODS.poll_object(args) if args.print then print(string.format("Key: %s, Weight: %s, Position: %s", weight_table.key, weight_table.weight, weight_table.mod_weight)) end end - local chance = args.guaranteed and 1 or ((args.chance or SMODS.base_rate_percentage[args.type] or 1) * (args.mod or 1) * (modded_weight/total_weight)) + local chance = (args.guaranteed or not (args.chance or SMODS.base_rate_percentage[args.type])) and 1 or ((args.chance or SMODS.base_rate_percentage[args.type] or 1) * (args.mod or 1) * (modded_weight/total_weight)) -- Adjust chance based on modified weightings -- chance = chance * (modded_weight/total_weight) local output_key = 'UNAVAILABLE' From 2100b33456b6d53f1a47bfc0bdfded72ffe1589c Mon Sep 17 00:00:00 2001 From: Eremel Date: Tue, 31 Mar 2026 23:37:47 +0100 Subject: [PATCH 29/32] (: --- src/overrides.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/overrides.lua b/src/overrides.lua index 8695df9a6..2cc9bf21f 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -2453,7 +2453,7 @@ function get_pack(_key, _type) filter = _type and function(pool) local out = {} for _, v in ipairs(pool) do - if G.P_CENTERS[v.key].kind == _type then + if G.P_CENTERS[v.key] and G.P_CENTERS[v.key].kind == _type then out[#out + 1] = v end end From b3ace60f4c5d8b7f2d390b70aab27926b5af4b41 Mon Sep 17 00:00:00 2001 From: Eremel Date: Wed, 1 Apr 2026 00:44:30 +0100 Subject: [PATCH 30/32] Address wilson --- src/game_objects/attributes.lua | 79 ++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 6 deletions(-) diff --git a/src/game_objects/attributes.lua b/src/game_objects/attributes.lua index 01aeb5244..fec957b41 100644 --- a/src/game_objects/attributes.lua +++ b/src/game_objects/attributes.lua @@ -112,10 +112,10 @@ SMODS.Attribute({ SMODS.Attribute({ key = 'scaling', keys = { - 'j_ceremonial', 'j_steel', 'j_ride_the_bus', 'j_egg', 'j_runner', + 'j_ceremonial', 'j_selzer', 'j_ride_the_bus', 'j_egg', 'j_runner', 'j_ice_cream', 'j_constellation', 'j_hiker', 'j_green_joker', 'j_red_card', - 'j_madness', 'j_square', 'j_vampire', 'j_hologram', 'j_rocket', - 'j_obelisk', 'j_gift', 'j_fortune_teller', 'j_stone', 'j_flash', + 'j_madness', 'j_square', 'j_vampire', 'j_hologram', 'j_rocket', 'j_turtle_bean', + 'j_obelisk', 'j_gift', 'j_flash', 'j_lucky_cat', 'j_popcorn', 'j_trousers', 'j_ramen', 'j_castle', 'j_campfire', 'j_throwback', 'j_glass', 'j_wee', 'j_hit_the_road', 'j_caino', 'j_yorick' } @@ -134,7 +134,7 @@ SMODS.Attribute({ key = 'suit', keys = { 'j_greedy_joker', 'j_lusty_joker', 'j_wrathful_joker', 'j_gluttenous_joker', - 'j_smeared', 'j_castle', 'j_ancient', 'j_seeing_double', 'j_blackboard', 'j_flower_pot', + 'j_smeared', 'j_castle', 'j_ancient', 'j_seeing_double', 'j_blackboard', 'j_flower_pot', 'j_idol', 'j_rough_gem', 'j_bloodstone', 'j_arrowhead', 'j_onyx_agate', } }) @@ -175,6 +175,7 @@ SMODS.Attribute({ 'j_four_fingers', 'j_supernova', 'j_runner', 'j_superposition', 'j_todo_list', 'j_seance', 'j_shortcut', 'j_obelisk', 'j_trousers', 'j_duo', 'j_trio', 'j_family', 'j_order', 'j_tribe', + 'j_burnt', 'j_card_sharp', 'j_space' } }) @@ -282,7 +283,7 @@ SMODS.Attribute({ SMODS.Attribute({ key = 'face', keys = { - 'j_scary', 'j_pareidolia', 'j_business', 'j_ride_the_bus', + 'j_scary_face', 'j_pareidolia', 'j_business', 'j_ride_the_bus', 'j_faceless', 'j_midas_mask', 'j_photograph', 'j_reserved_parking', 'j_smiley', 'j_sock_and_buskin', 'j_caino' } @@ -298,7 +299,8 @@ SMODS.Attribute({ SMODS.Attribute({ key = 'food', keys = { - 'j_gros_michel', 'j_cavendish', 'j_ice_cream', 'j_ramen', 'j_turtle_bean', 'j_popcorn', 'j_seltzer' + 'j_gros_michel', 'j_cavendish', 'j_ice_cream', 'j_ramen', 'j_turtle_bean', 'j_popcorn', 'j_selzer', + 'j_egg', 'j_diet_cola' } }) @@ -362,4 +364,69 @@ SMODS.Attribute({ keys = { 'j_sixth_sense', 'j_seance' } +}) + +SMODS.Attribute({ + key = 'joker', + keys = { "j_abstract", "j_riff_raff", "j_swashbuckler" } +}) + +SMODS.Attribute({ + key = 'joker_slot', + keys = { "j_abstract", "j_stencil" } +}) + +SMODS.Attribute({ + key = 'destroy_card', + keys = { "j_ceremonial", "j_madness", "j_trading", "j_mr_bones" } +}) + +SMODS.Attribute({ + key = 'hands', + keys = { "j_loyalty_card", "j_burglar", "j_troubadour", "j_green_joker", "j_square", "j_dusk", "j_acrobat", "j_flower_pot", "j_dna", "j_vagabond", "j_obelisk" } +}) + +SMODS.Attribute({ + key = 'reset', + keys = { "j_obelisk", "j_campfire", "j_hit_the_road", "j_ride_the_bus" } +}) + +SMODS.Attribute({ + key = 'enhancements', + keys = { "j_ticket", "j_marble", "j_steel_joker", "j_vampire", "j_midas_mask", "j_stone", "j_lucky_cat", "j_glass", "j_drivers_license" } +}) + +SMODS.Attribute({ + key = 'modify_card', + keys = { "j_pareidolia", "j_hiker", "j_vampire", "j_midas_mask" } +}) + +SMODS.Attribute({ + key = 'prevents_death', + keys = { "j_mr_bones" } +}) + +SMODS.Attribute({ + key = 'seals', + keys = { "j_certificate" } +}) + +SMODS.Attribute({ + key = 'hand_size', + keys = { "j_juggler", "j_turtle_bean", "j_troubadour", "j_merry_andy", "j_stuntman" } +}) + +SMODS.Attribute({ + key = 'reroll', + keys = { "j_chaos", "j_flash" } +}) + +SMODS.Attribute({ + key = 'sell_value', + keys = { "j_egg", "j_swashbuckler", "j_ceremonial", "j_gift" } +}) + +SMODS.Attribute({ + key = 'full_deck', + keys = { "j_steel_joker", "j_cloud_9", "j_erosion", "j_stone", "j_drivers_license" } }) \ No newline at end of file From 28b5fe4e8d0f85c7560b18bab8426a543e2853d0 Mon Sep 17 00:00:00 2001 From: Eremel Date: Wed, 1 Apr 2026 10:26:42 +0100 Subject: [PATCH 31/32] Add passive attribute --- src/game_objects/attributes.lua | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/game_objects/attributes.lua b/src/game_objects/attributes.lua index fec957b41..74a69f992 100644 --- a/src/game_objects/attributes.lua +++ b/src/game_objects/attributes.lua @@ -378,7 +378,16 @@ SMODS.Attribute({ SMODS.Attribute({ key = 'destroy_card', - keys = { "j_ceremonial", "j_madness", "j_trading", "j_mr_bones" } + keys = { "j_ceremonial", "j_madness", "j_trading" } +}) + +SMODS.Attribute({ + key = 'passive', + keys = { + 'j_four_fingers', 'j_credit_card', 'j_chaos', 'j_pareidolia', 'j_splash', + 'j_shortcut', 'j_to_the_moon', 'j_juggler', 'j_drunkard', 'j_troubadour', + 'j_smeared', 'j_ring_master', 'j_oops', 'j_astronomer' + } }) SMODS.Attribute({ From 6ed5463e4b1b47c9818f9c16bcea36d13df37128 Mon Sep 17 00:00:00 2001 From: Eremel Date: Wed, 1 Apr 2026 11:02:32 +0100 Subject: [PATCH 32/32] Add on_sell attribute --- src/game_objects/attributes.lua | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/game_objects/attributes.lua b/src/game_objects/attributes.lua index 74a69f992..fa2e23829 100644 --- a/src/game_objects/attributes.lua +++ b/src/game_objects/attributes.lua @@ -241,7 +241,7 @@ SMODS.Attribute({ SMODS.Attribute({ key = 'eight', keys = { - 'j_8_ball', 'j_even_steven' + 'j_8_ball', 'j_even_steven', 'j_fibonacci' } }) @@ -438,4 +438,11 @@ SMODS.Attribute({ SMODS.Attribute({ key = 'full_deck', keys = { "j_steel_joker", "j_cloud_9", "j_erosion", "j_stone", "j_drivers_license" } +}) + +SMODS.Attribute({ + key = 'on_sell', + keys = { + 'j_luchador', 'j_diet_cola', 'j_invisible' + } }) \ No newline at end of file