diff --git a/lovely/card_limit.toml b/lovely/card_limit.toml index 5e91df139..366ed0137 100644 --- a/lovely/card_limit.toml +++ b/lovely/card_limit.toml @@ -286,7 +286,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/lovely/weights.toml b/lovely/weights.toml new file mode 100644 index 000000000..d2e1f1b18 --- /dev/null +++ b/lovely/weights.toml @@ -0,0 +1,84 @@ +[manifest] +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 +''' + +# 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 = ''' +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 + +''' + +# 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] +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 +''' \ No newline at end of file diff --git a/src/core.lua b/src/core.lua index bf626936d..9102d8599 100644 --- a/src/core.lua +++ b/src/core.lua @@ -6,6 +6,7 @@ for _, path in ipairs { "src/overrides.lua", "src/game_object.lua", "src/compat_0_9_8.lua", + "src/utils/weights.lua" } do assert(load(SMODS.NFS.read(SMODS.path..path), ('=[SMODS _ "%s"]'):format(path)))() end diff --git a/src/game_object.lua b/src/game_object.lua index b8f6b4b9f..e6bcd7139 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 @@ -389,6 +393,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 +1198,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..fa2e23829 --- /dev/null +++ b/src/game_objects/attributes.lua @@ -0,0 +1,448 @@ +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_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' + } +}) + +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' + } +}) + +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_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_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' + } +}) + +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_idol', + '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', + 'j_burnt', 'j_card_sharp', 'j_space' + } +}) + +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_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', 'j_fibonacci' + } +}) + +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 = { + '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' + } +}) + +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_selzer', + 'j_egg', 'j_diet_cola' + } +}) + +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', + } +}) + +SMODS.Attribute({ + key = 'mod_chance', + 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' + } +}) + +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" } +}) + +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({ + 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" } +}) + +SMODS.Attribute({ + key = 'on_sell', + keys = { + 'j_luchador', 'j_diet_cola', 'j_invisible' + } +}) \ No newline at end of file diff --git a/src/overrides.lua b/src/overrides.lua index fd8eb1983..2cc9bf21f 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -2223,6 +2223,14 @@ 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) + 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 + + -- 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 + + 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 @@ -2438,6 +2446,20 @@ 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', + filter = _type and function(pool) + local out = {} + for _, v in ipairs(pool) do + if G.P_CENTERS[v.key] and 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 = {} for k, v in ipairs(G.P_CENTER_POOLS['Booster']) do @@ -2675,6 +2697,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/preflight/loader.lua b/src/preflight/loader.lua index f7156c184..7542a3d4d 100644 --- a/src/preflight/loader.lua +++ b/src/preflight/loader.lua @@ -886,6 +886,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 f37c9b878..92698762a 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -372,6 +372,10 @@ 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 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 @@ -672,6 +676,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] = (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 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) @@ -693,10 +717,13 @@ 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) + -- 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 {} local key = args.key or 'stdseal' local mod = args.mod or 1 @@ -712,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 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 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 } @@ -2215,18 +2242,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, set = 'Back', key = G.GAME.selected_back.effect.center.key }, + { object = G.GAME.selected_back, scored_card = G.deck and G.deck.cards[1] or G.deck, set = 'Back', key = G.GAME.selected_back.effect.center.key }, } 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, set = "Blind", key = G.GAME.blind.config.blind.key} end - if G.GAME.challenge then t[#t + 1] = { object = SMODS.Challenges[G.GAME.challenge], scored_card = G.deck.cards[1] or G.deck, set = "Challenge", key = SMODS.Challenges[G.GAME.challenge].id } 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, set = "Challenge", key = SMODS.Challenges[G.GAME.challenge].id } end for _, stake in ipairs(SMODS.get_stake_scoring_targets(_context)) do - t[#t + 1] = { object = stake, scored_card = G.deck.cards[1] or G.deck, set = "Stake", key = stake.key } + t[#t + 1] = { object = stake, scored_card = G.deck and G.deck.cards[1] or G.deck, set = "Stake", key = stake.key } end for _, mod in ipairs(SMODS.get_mods_scoring_targets(_context)) do - t[#t + 1] = { object = mod, scored_card = G.deck.cards[1] or G.deck, set = "Mod", key = mod.id } + t[#t + 1] = { object = mod, scored_card = G.deck and G.deck.cards[1] or G.deck, set = "Mod", key = mod.id } end -- TARGET: add your own individual scoring targets return t @@ -2430,11 +2457,18 @@ 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)) + local center + + -- Use SMODS object weight system when enabled + 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 @@ -3411,7 +3445,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 @@ -3421,7 +3455,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 new file mode 100644 index 000000000..88378ed43 --- /dev/null +++ b/src/utils/weights.lua @@ -0,0 +1,479 @@ +-- 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?} +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.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))) + + -- Prepare pool + local pool = args.pool or {} + local types = args.attributes or args.types or {args.type} + + -- Populate pool + local types_used = {} + + if not args.pool then + pool, types_used = SMODS.create_poll_pool(types, args) + 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 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) + 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 + + if args.print then print(string.format("Key: %s, Base weight: %s, Final weight: %s", weight_table.key, w, weight_table.weight)) end + 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 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' + + 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 + if args.print then print('Modded Weight:'..modded_weight) end + if args.print then print('Base Chance: '..chance) end + + 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(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 + if args.print then print('Looking for item: '..poll_weight) end + + if poll_weight > final_pool[1].mod_weight then + output_key = final_pool[SMODS.select_by_weight(final_pool, poll_weight, 1, #final_pool)].key + else + output_key = final_pool[1].key + end + end + + -- Edition specific functionality + 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 +---@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 + + 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 = 'Enhanced', 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(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[blind_type] then + elseif options.ignore_showdown_check then + eligible_bosses[k] = res and true or nil + elseif blind_type == 'boss' then + if + ((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[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 + 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 + 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 + if eligible_bosses[k] then + eligible_bosses[k] = v + if eligible_bosses[k] <= min_use then + min_use = eligible_bosses[k] + 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 + + local output = {} + for k, _ in pairs(eligible_bosses) do + output[#output + 1] = k + end + + 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 = {} + 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 + + 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 + local temp_pool = {} + 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 + _p = SMODS.cull_pool(_p, args) + end + 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_lists({temp_pool, _p}) + end + for _, v in ipairs(temp_pool) do + pool[v] = {key = v, type = label} + end + 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 + 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 + 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 + +function SMODS.is_showdown_ante() + return G.GAME.round_resets.ante%G.GAME.win_ante == 0 and G.GAME.round_resets.ante > 0 +end + +-- New create_card_for_shop structure +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 + 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) + 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 + if SMODS.debug_prints then print(weighted_table[ind].type) end + return weighted_table[ind].type +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)) and (not _rarity or _rarity == v.rarity) 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 + + 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 + + + return final_pool +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