diff --git a/lovely/better_calc.toml b/lovely/better_calc.toml index f6c522f9f..3caf58a84 100644 --- a/lovely/better_calc.toml +++ b/lovely/better_calc.toml @@ -35,9 +35,9 @@ payload = ''' function eval_card(card, context) context = context or {} if not card:can_calculate(context.ignore_debuff, context.remove_playing_cards or context.joker_type_destroyed) then - if card.ability.rental then + if card:has_sticker("rental", {bypass_debuff = true}) then local ret = {} - ret[SMODS.Stickers.rental] = card:calculate_sticker(context, 'rental') + ret[SMODS.Stickers.rental] = SMODS.Stickers.rental:calculate(card, context) return ret, {} end return {}, {} @@ -48,7 +48,7 @@ function eval_card(card, context) local ret = {} ''' -# built in config values +# functions/common_events.lua : eval_card() : replace vanilla card field calc with CardAbilityFields. [[patches]] [patches.pattern] target = "functions/common_events.lua" @@ -88,53 +88,6 @@ end match_indent = true position = "at" payload = """ -if context.cardarea == G.play and context.main_scoring then - ret.playing_card = {} - local chips = card:get_chip_bonus() - if chips ~= 0 then - ret.playing_card.chips = chips - end - - local mult = card:get_chip_mult() - if mult ~= 0 then - ret.playing_card.mult = mult - end - - local x_mult = card:get_chip_x_mult(context) - if x_mult > 0 then - ret.playing_card.x_mult = x_mult - end - - local p_dollars = card:get_p_dollars() - if p_dollars ~= 0 then - ret.playing_card.p_dollars = p_dollars - end - - local x_chips = card:get_chip_x_bonus() - if x_chips > 0 then - ret.playing_card.x_chips = x_chips - end - - local score = card:get_bonus_score() - if score ~= 0 then - ret.playing_card.score = score - end - local x_score = card:get_bonus_x_score() - if x_score > 0 then - ret.playing_card.x_score = x_score - end - - local blind_size = card:get_bonus_blind_size() - if blind_size ~= 0 then - ret.playing_card.blind_size = blind_size - end - local x_blind_size = card:get_bonus_x_blind_size() - if x_blind_size > 0 then - ret.playing_card.x_blind_size = x_blind_size - end - - -- TARGET: main scoring on played cards -end if context.end_of_round and context.cardarea == G.hand and context.playing_card_end_of_round then local end_of_round = card:get_end_of_round_effect(context) if end_of_round then @@ -165,48 +118,18 @@ if context.cardarea == G.hand then match_indent = true position = "at" payload = """ -if context.cardarea == G.hand and context.main_scoring then - ret.playing_card = {} - local h_mult = card:get_chip_h_mult() - if h_mult ~= 0 then - ret.playing_card.h_mult = h_mult - end - - local h_x_mult = card:get_chip_h_x_mult() - if h_x_mult > 0 then - ret.playing_card.x_mult = h_x_mult - end - - local h_chips = card:get_chip_h_bonus() - if h_chips ~= 0 then - ret.playing_card.h_chips = h_chips - end - - local h_x_chips = card:get_chip_h_x_bonus() - if h_x_chips > 0 then - ret.playing_card.x_chips = h_x_chips - end - - local h_score = card:get_bonus_h_score() - if h_score ~= 0 then - ret.playing_card.h_score = h_score - end - local h_x_score = card:get_bonus_h_x_score() - if h_x_score > 0 then - ret.playing_card.h_x_score = h_x_score - end - local h_blind_size = card:get_bonus_h_blind_size() - if h_blind_size ~= 0 then - ret.playing_card.blind_size = h_blind_size - end - local h_x_blind_size = card:get_bonus_h_x_blind_size() - if h_x_blind_size > 0 then - ret.playing_card.x_blind_size = h_x_blind_size +if context.main_scoring or context.end_of_round then + for i, key in ipairs(SMODS.CardAbilityField.obj_buffer) do + local ca_field = SMODS.CardAbilityFields[key] + if ca_field:check_context_criteria(context) then + ca_field:insert_value(card, ret) + end end - +end +if context.cardarea == G.hand and context.main_scoring then -- TARGET: main scoring on held cards """ - +# functions/common_events.lua : eval_card() : replace vanilla card field calc with CardAbilityFields and QuantumCardFields [[patches]] [patches.pattern] target = "functions/common_events.lua" @@ -219,42 +142,14 @@ end match_indent = true position = "at" payload = """ -if card.ability.set == 'Enhanced' then - local enhancement = card:calculate_enhancement(context) - if enhancement then - ret.enhancement = enhancement - end -end -if context.extra_enhancement then return ret end -if card.edition then - local edition = card:calculate_edition(context) - if edition then - ret.edition = edition - end -end -if card.seal then - local seals = card:calculate_seal(context) - if seals then - ret.seals = seals - end -end -for _,k in ipairs(SMODS.Sticker.obj_buffer) do - local v = SMODS.Stickers[k] - local sticker = card:calculate_sticker(context, k) - if sticker then - ret[v] = sticker - end +for i, key in ipairs(SMODS.QuantumCardField.obj_buffer) do + local q_field = SMODS.QuantumCardFields[key] + local eval = q_field.calculate(card, context) + if eval then ret[q_field.calc_key] = eval end end -- TARGET: evaluate your own repetition effects -if card.ability.repetitions and card.ability.repetitions > 0 then - ret.seals = ret.seals or { card = card, message = localize('k_again_ex') } - ret.seals.repetitions = (ret.seals.repetitions and ret.seals.repetitions + card.ability.repetitions) or card.ability.repetitions -end -if card.ability.perma_repetitions and card.ability.perma_repetitions > 0 then - ret.seals = ret.seals or { card = card, message = localize('k_again_ex') } - ret.seals.repetitions = (ret.seals.repetitions and ret.seals.repetitions + card.ability.perma_repetitions) or card.ability.perma_repetitions -end +SMODS.CardAbilityFields.repetitions:insert_value(card, ret) """ [[patches]] [patches.pattern] @@ -263,31 +158,10 @@ pattern = "if context.cardarea == G.jokers or context.card == G.consumeables the match_indent = true position = "before" payload = """ -if card.ability.set == 'Enhanced' then - local enhancement = card:calculate_enhancement(context) - if enhancement then - ret.enhancement = enhancement - end -end -if context.extra_enhancement then return ret end -if card.edition then - local edition = card:calculate_edition(context) - if edition then - ret.edition = edition - end -end -if card.seal then - local seals = card:calculate_seal(context) - if seals then - ret.seals = seals - end -end -for _,k in ipairs(SMODS.Sticker.obj_buffer) do - local v = SMODS.Stickers[k] - local sticker = card:calculate_sticker(context, k) - if sticker then - ret[v] = sticker - end +for i, key in ipairs(SMODS.QuantumCardField.obj_buffer) do + local q_field = SMODS.QuantumCardFields[key] + local eval = q_field.calculate(card, context) + if eval then ret[q_field.calc_key] = eval end end -- TARGET: evaluate your own general effects @@ -1510,7 +1384,6 @@ payload = ''' local card_to_save = self and copy_card(self) or Card(0, 0, G.CARD_W, G.CARD_H, G.P_CARDS.empty, center) card_to_save.VT.x, card_to_save.VT.y = G.vouchers.T.x, G.vouchers.T.y G.vouchers:emplace(card_to_save) - SMODS.enh_cache:clear() ''' [[patches]] diff --git a/lovely/calculate_card_added.toml b/lovely/calculate_card_added.toml index 7a5561ad5..f817aaabb 100644 --- a/lovely/calculate_card_added.toml +++ b/lovely/calculate_card_added.toml @@ -21,7 +21,6 @@ if not from_debuff and G.hand then if not is_playing_card then SMODS.calculate_context({card_added = true, card = self}) - SMODS.enh_cache:clear() end end ''' diff --git a/lovely/can_calculate.toml b/lovely/can_calculate.toml index 3b2c01eb2..cc2d5f9d5 100644 --- a/lovely/can_calculate.toml +++ b/lovely/can_calculate.toml @@ -3,16 +3,6 @@ version = "1.0.0" dump_lua = true priority = -5 -[[patches]] -[patches.pattern] -target = "card.lua" -pattern = '''function Card:get_end_of_round_effect(context) - if self.debuff then return {} end''' -position = "at" -match_indent = true -payload = ''' -function Card:get_end_of_round_effect(context)''' - [[patches]] [patches.pattern] target = "card.lua" @@ -24,16 +14,6 @@ payload = ''' function Card:calculate_dollar_bonus() if not self:can_calculate() then return end''' -[[patches]] -[patches.pattern] -target = "card.lua" -pattern = '''function Card:calculate_seal(context) - if self.debuff then return nil end''' -position = "at" -match_indent = true -payload = ''' -function Card:calculate_seal(context)''' - [[patches]] [patches.pattern] target = "card.lua" diff --git a/lovely/center.toml b/lovely/center.toml index b6595805a..8434a1fac 100644 --- a/lovely/center.toml +++ b/lovely/center.toml @@ -79,7 +79,16 @@ match_indent = true payload = ''' local obj = self.config.center if obj.update and type(obj.update) == 'function' then - obj:update(self, dt) + if G.STAGE == G.STAGES.RUN then + if self.ability and self.ability.perma_debuff then self.debuff = true end + if self.area and ((self.area == G.jokers) or (self.area == G.consumeables)) then + self.bypass_lock = true + self.bypass_discovery_center = true + self.bypass_discovery_ui = true + end + self.sell_cost_label = self.facing == 'back' and '?' or self.sell_cost + end + return obj:update(self, dt) end''' # Card:generate_UIBox_ability_table() diff --git a/lovely/enhancement.toml b/lovely/enhancement.toml index 0593075f0..6c9001213 100644 --- a/lovely/enhancement.toml +++ b/lovely/enhancement.toml @@ -14,20 +14,6 @@ match_indent = true position = "at" payload = '''if SMODS.has_no_rank(self) and not self.vampired then''' -# Card:get_chip_bonus() -[[patches]] -[patches.regex] -target = "card.lua" -pattern = ''' -(?[\t ]*)if self\.ability\.effect == 'Stone Card' then -[\t ]* return self\.ability\.bonus \+ \(self\.ability\.perma_bonus or 0\) -[\t ]*end''' -position = "at" -payload = '''if self.ability.effect == 'Stone Card' or self.config.center.replace_base_card then - return self.ability.bonus + (self.ability.perma_bonus or 0) -end''' -line_prepend = '$indent' - # Card:calculate_joker() # Raised Fist [[patches]] @@ -158,45 +144,6 @@ match_indent = true position = "at" payload = "elseif specific_vars.playing_card then" - -## Allow cards to function as multiple enhancements (e.g. from jokers) -# Calculate extra enhancements when held in hand at end of round -[[patches]] -[patches.pattern] -target = "functions/state_events.lua" -pattern = "local effects = {G.hand.cards[i]:get_end_of_round_effect()}" -position = "at" -payload = ''' -local effects = {[1] = {playing_card = G.hand.cards[i]:get_end_of_round_effect()}} -local extra_enhancements = SMODS.get_enhancements(G.hand.cards[i], true) -local old_ability = copy_table(G.hand.cards[i].ability) -local old_center = G.hand.cards[i].config.center -local old_center_key = G.hand.cards[i].config.center_key -for k, _ in pairs(extra_enhancements) do - if G.P_CENTERS[k] then - G.hand.cards[i]:set_ability(G.P_CENTERS[k]) - G.hand.cards[i].ability.extra_enhancement = k - effects[#effects+1] = {[1] = {playing_card = G.hand.cards[i]:get_end_of_round_effect()}} - end -end -G.hand.cards[i].ability = old_ability -G.hand.cards[i].config.center = old_center -G.hand.cards[i].config.center_key = old_center_key -G.hand.cards[i]:set_sprites(old_center) -''' -match_indent = true - -# Prevent blue seal effect on extra enhancements at end of round -[[patches]] -[patches.pattern] -target = "card.lua" -pattern = "if self.seal == 'Blue' and #G.consumeables.cards + G.GAME.consumeable_buffer < G.consumeables.config.card_limit then" -position = "before" -payload = ''' -if self.extra_enhancement then return ret end -''' -match_indent = true - # Use the has enhancement function for enhancement gates [[patches]] [patches.pattern] @@ -239,22 +186,13 @@ position = "at" payload = "if cards_destroyed[i].shattered then" match_indent = true -# Prevent blue seals from applying on quantum enhancement calc +# card.lua : Card:calculate_end_of_round_effect() : update to use quantum check [[patches]] [patches.pattern] target = "card.lua" pattern = "if self.seal == 'Blue' and #G.consumeables.cards + G.GAME.consumeable_buffer < G.consumeables.config.card_limit then" position = "at" -payload = "if self.seal == 'Blue' and #G.consumeables.cards + G.GAME.consumeable_buffer < G.consumeables.config.card_limit and not self.ability.extra_enhancement then" -match_indent = true - -# Reset enh cache on game update -[[patches]] -[patches.pattern] -target = "game.lua" -pattern = "modulate_sound(dt)" -position = "after" -payload = "SMODS.enh_cache:clear()" +payload = "if self:has_seal('Blue') and #G.consumeables.cards + G.GAME.consumeable_buffer < G.consumeables.config.card_limit then" match_indent = true # Invalidate enhancement cache when card changes / replace_base_card fix pt1 @@ -264,7 +202,6 @@ target = "card.lua" pattern = "function Card:set_ability(center, initial, delay_sprites)" position = "after" payload = ''' - SMODS.enh_cache:write(self, nil) if self.ability and not initial then self.ability.card_limit = self.ability.card_limit - (self.config.center.config.card_limit or 0) @@ -299,13 +236,6 @@ payload = '''if self.ability and not initial and not delay_sprites then self.front_hidden = self:should_hide_front() end''' match_indent = true -[[patches]] -[patches.pattern] -target = "card.lua" -pattern = "function Card:set_base(card, initial)" -position = "after" -payload = "SMODS.enh_cache:write(self, nil)" -match_indent = true [[patches]] [patches.pattern] @@ -313,7 +243,6 @@ target = "card.lua" pattern = "function Card:set_seal(_seal, silent, immediate)" position = "after" payload = ''' - SMODS.enh_cache:write(self, nil) if self.seal then self.ability.card_limit = self.ability.card_limit - (self.ability.seal.card_limit or 0) self.ability.extra_slots_used = self.ability.extra_slots_used - (self.ability.seal.extra_slots_used or 0) diff --git a/lovely/fixes.toml b/lovely/fixes.toml index e03c94e54..400a48d7f 100644 --- a/lovely/fixes.toml +++ b/lovely/fixes.toml @@ -424,7 +424,7 @@ payload = '''if win_ante and (win_ante >= 8) or not SMODS.add_to_pool(v) then''' target = "card.lua" pattern = 'if G\.GAME\.blind then' position = "at" -payload = "if G.GAME.blind and G.GAME.blind.in_blind and not self.from_quantum then" +payload = "if G.GAME.blind and G.GAME.blind.in_blind then" # end_round() [[patches]] @@ -503,24 +503,6 @@ payload = 'for k, v in pairs(G.I.SPRITE) do' ## Make vanilla enhancement jokers work with extra enhancements -# Steel Joker -[[patches]] -[patches.pattern] -target = "card.lua" -match_indent = true -pattern = "if v.config.center == G.P_CENTERS.m_steel then self.ability.steel_tally = self.ability.steel_tally+1 end" -position = "at" -payload = "if SMODS.has_enhancement(v, 'm_steel') then self.ability.steel_tally = self.ability.steel_tally+1 end" - -# Stone Joker -[[patches]] -[patches.pattern] -target = "card.lua" -match_indent = true -pattern = "if v.config.center == G.P_CENTERS.m_stone then self.ability.stone_tally = self.ability.stone_tally+1 end" -position = "at" -payload = "if SMODS.has_enhancement(v, 'm_stone') then self.ability.stone_tally = self.ability.stone_tally+1 end" - # Golden Ticket [[patches]] [patches.pattern] @@ -548,14 +530,6 @@ pattern = "if val.ability.name == 'Glass Card' then shattered_glass = shattered_ position = "at" payload = "if SMODS.has_enhancement(val, 'm_glass') then shattered_glass = shattered_glass + 1 end" -# Driver's License -[[patches]] -[patches.pattern] -target = "card.lua" -match_indent = true -pattern = "if v.config.center ~= G.P_CENTERS.c_base then self.ability.driver_tally = self.ability.driver_tally+1 end" -position = "at" -payload = "if next(SMODS.get_enhancements(v)) then self.ability.driver_tally = self.ability.driver_tally+1 end" # Basegame fix for the reroll vouchers redeem function when only the center but no card object exists # Card:apply_to_run @@ -722,15 +696,6 @@ for k,v in pairs(other.edition or {}) do end end''' -# fix Smeared Joker compat issues with modded suits -[[patches]] -[patches.pattern] -target = "card.lua" -pattern = "if next(find_joker('Smeared Joker')) and (self.base.suit == 'Hearts' or self.base.suit == 'Diamonds') == (suit == 'Hearts' or suit == 'Diamonds') then" -position = "at" -payload = "if SMODS.smeared_check(self, suit) then" -match_indent = true - # fix Seeing Double compat issues with modded suits [[patches]] [patches.pattern] @@ -775,16 +740,6 @@ payload = """if SMODS.seeing_double_check(context.scoring_hand, 'Clubs') then end""" match_indent = true -# Card:get_end_of_round_effect -# prevents Blue Seal crash from a hand with no matching planet card (spawns random planet card) -[[patches]] -[patches.pattern] -target = "card.lua" -pattern = """local card = create_card(card_type,G.consumeables, nil, nil, nil, nil, _planet, 'blusl')""" -position = 'before' -payload = """if _planet == 0 then _planet = nil end""" -match_indent = true - ## Fix remove_from_deck being called and unlocking stuff when exiting a run and ## clearing screen # Game:delete_run() diff --git a/lovely/override_notice.toml b/lovely/override_notice.toml index 72157e6ba..f23ce553b 100644 --- a/lovely/override_notice.toml +++ b/lovely/override_notice.toml @@ -65,6 +65,22 @@ position = "before" payload = "-- Function overridden by SMODS in src/overrides.lua" match_indent = true +[[patches]] +[patches.pattern] +target = "functions/misc_functions.lua" +pattern = "function get_flush(hand)" +position = "before" +payload = "-- Function overridden by SMODS in src/overrides.lua" +match_indent = true + +[[patches]] +[patches.pattern] +target = "functions/misc_functions.lua" +pattern = "function get_X_same(num, hand)" +position = "before" +payload = "-- Function overridden by SMODS in src/overrides.lua" +match_indent = true + [[patches]] [patches.pattern] target = "functions/misc_functions.lua" @@ -150,6 +166,22 @@ match_indent = true target = "card.lua" pattern = "function Card:set_edition(edition, immediate, silent)" position = "before" +payload = "-- Function overridden by SMODS in src/game_objects/quantum_card_fields.lua (QField.inject)" +match_indent = true + +[[patches]] +[patches.pattern] +target = "card.lua" +pattern = "function Card:set_seal(_seal, silent, immediate)" +position = "before" +payload = "-- Function overridden by SMODS in src/game_objects/quantum_card_fields.lua (QField.inject)" +match_indent = true + +[[patches]] +[patches.pattern] +target = "card.lua" +pattern = "function Card:get_end_of_round_effect(context)" +position = "before" payload = "-- Function overridden by SMODS in src/overrides.lua" match_indent = true diff --git a/lovely/perma_bonus.toml b/lovely/perma_bonus.toml index 4addb78e7..f8ac9eafc 100644 --- a/lovely/perma_bonus.toml +++ b/lovely/perma_bonus.toml @@ -153,210 +153,3 @@ perma_x_blind_size = self.ability and self.ability.perma_x_blind_size or 0, perma_h_x_blind_size = self.ability and self.ability.perma_h_x_blind_size or 0, ''' match_indent = true - -# Card:get_chip_bonus -[[patches]] -[patches.pattern] -target = "card.lua" -pattern = '''function Card:get_chip_bonus(*''' -position = "after" -match_indent = true -payload = ''' - if self.ability.extra_enhancement then return self.ability.bonus end''' - -# Card:get_chip_mult -[[patches]] -[patches.pattern] -target = "card.lua" -pattern = '''if self.ability.effect == "Lucky Card" then - if pseudorandom('lucky_mult') < G.GAME.probabilities.normal/5 then - self.lucky_trigger = true - return self.ability.mult - else - return 0 - end -else - return self.ability.mult -end''' -position = "at" -match_indent = true -payload = '''local ret = (not self.ability.extra_enhancement and self.ability.perma_mult) or 0 -if self.ability.effect == "Lucky Card" then - if SMODS.pseudorandom_probability(self, 'lucky_mult', 1, 5) then - self.lucky_trigger = true - ret = ret + self.ability.mult - end -else - ret = ret + self.ability.mult -end --- TARGET: get_chip_mult -return ret''' - -# Card:get_chip_x_mult -[[patches]] -[patches.pattern] -target = "card.lua" -pattern = '''if self.ability.x_mult <= 1 then return 0 end -return self.ability.x_mult''' -position = "at" -match_indent = true -payload = '''local ret = SMODS.multiplicative_stacking(self.ability.x_mult or 1, (not self.ability.extra_enhancement and self.ability.perma_x_mult) or 0) --- TARGET: get_chip_x_mult -return ret -''' - -# Card:get_chip_h_mult -[[patches]] -[patches.pattern] -target = "card.lua" -pattern = 'return self.ability.h_mult' -position = "at" -match_indent = true -payload = '''local ret = (self.ability.h_mult or 0) + ((not self.ability.extra_enhancement and self.ability.perma_h_mult) or 0) --- TARGET: get_chip_h_mult -return ret -''' - -# Card:get_chip_h_x_mult -[[patches]] -[patches.pattern] -target = "card.lua" -pattern = 'return self.ability.h_x_mult' -position = "at" -match_indent = true -payload = '''local ret = SMODS.multiplicative_stacking(self.ability.h_x_mult or 1, (not self.ability.extra_enhancement and self.ability.perma_h_x_mult) or 0) --- TARGET: get_chip_h_x_mult -return ret -''' - -# Card:get_chip_x_bonus -# Card:get_chip_h_bonus -# Card:get_chip_h_x_bonus -# Card:get_h_dollars -[[patches]] -[patches.pattern] -target = "card.lua" -pattern = 'function Card:get_edition()' -position = "before" -match_indent = true -payload = ''' -function Card:get_chip_x_bonus() - if self.debuff then return 0 end - local ret = SMODS.multiplicative_stacking(self.ability.x_chips or 1, (not self.ability.extra_enhancement and self.ability.perma_x_chips) or 0) - -- TARGET: get_chip_x_bonus - return ret -end - -function Card:get_chip_h_bonus() - if self.debuff then return 0 end - local ret = (self.ability.h_chips or 0) + ((not self.ability.extra_enhancement and self.ability.perma_h_chips) or 0) - -- TARGET: get_chip_h_bonus - return ret -end - -function Card:get_chip_h_x_bonus() - if self.debuff then return 0 end - local ret = SMODS.multiplicative_stacking(self.ability.h_x_chips or 1, (not self.ability.extra_enhancement and self.ability.perma_h_x_chips) or 0) - -- TARGET: get_chip_h_x_bonus - return ret -end - -function Card:get_h_dollars() - if self.debuff then return 0 end - local ret = (self.ability.h_dollars or 0) + ((not self.ability.extra_enhancement and self.ability.perma_h_dollars) or 0) - -- TARGET: get_h_dollars - return ret -end - -function Card:get_bonus_score() - if self.debuff then return 0 end - local ret = (self.ability.perma_score or 0) - -- TARGET: get_bonus_score - return ret -end - -function Card:get_bonus_x_score() - if self.debuff then return 0 end - local ret = SMODS.multiplicative_stacking(1, self.ability.perma_x_score or 0) - -- TARGET: get_bonus_x_score - return ret -end - -function Card:get_bonus_h_score() - if self.debuff then return 0 end - local ret = (self.ability.perma_h_score or 0) - -- TARGET: get_bonus_h_score - return ret -end - -function Card:get_bonus_h_x_score() - if self.debuff then return 0 end - local ret = SMODS.multiplicative_stacking(1, self.ability.perma_h_x_score or 0) - -- TARGET: get_bonus_h_x_score - return ret -end -function Card:get_bonus_blind_size() - if self.debuff then return 0 end - local ret = (self.ability.perma_blind_size or 0) - -- TARGET: get_bonus_blind_size - return ret -end - -function Card:get_bonus_x_blind_size() - if self.debuff then return 1 end - local ret = SMODS.multiplicative_stacking(1, self.ability.perma_x_blind_size or 0) - -- TARGET: get_bonus_x_blind_size - return ret -end - -function Card:get_bonus_h_blind_size() - if self.debuff then return 0 end - local ret = (self.ability.perma_h_blind_size or 0) - -- TARGET: get_bonus_h_blind_size - return ret -end - -function Card:get_bonus_h_x_blind_size() - if self.debuff then return 1 end - local ret = SMODS.multiplicative_stacking(1, self.ability.perma_h_x_blind_size or 0) - -- TARGET: get_bonus_h_x_blind_size - return ret -end -''' - -# Card:get_p_dollars -[[patches]] -[patches.pattern] -target = "card.lua" -pattern = '''end -if ret > 0 then - G.GAME.dollar_buffer = (G.GAME.dollar_buffer or 0) + ret''' -position = "at" -match_indent = true -payload = '''elseif self.ability.p_dollars < 0 then - ret = ret + self.ability.p_dollars -end -ret = ret + ((not self.ability.extra_enhancement and self.ability.perma_p_dollars) or 0) --- TARGET: get_p_dollars -if ret ~= 0 then - G.GAME.dollar_buffer = (G.GAME.dollar_buffer or 0) + ret''' - -# Card:get_end_of_round_effect -[[patches]] -[patches.pattern] -target = "card.lua" -pattern = ''' -if self.ability.h_dollars > 0 then - ret.h_dollars = self.ability.h_dollars - ret.card = self -end -''' -position = "at" -match_indent = true -payload = ''' -local h_dollars = self:get_h_dollars() -if h_dollars ~= 0 then - ret.h_dollars = h_dollars - ret.card = self -end -''' \ No newline at end of file diff --git a/lovely/playing_card.toml b/lovely/playing_card.toml index b19a5880c..dfaac1333 100644 --- a/lovely/playing_card.toml +++ b/lovely/playing_card.toml @@ -121,75 +121,6 @@ payload = ''' G.ARGS.LOC_COLOURS[v:lower()] = G.C.SUITS[v] end''' -# get_flush() -[[patches]] -[patches.pattern] -target = "functions/misc_functions.lua" -pattern = ''' -local suits = { - "Spades", - "Hearts", - "Clubs", - "Diamonds" - } -''' -position = 'at' -payload = 'local suits = SMODS.Suit.obj_buffer' -match_indent = true - -[[patches]] -[patches.pattern] -target = "functions/misc_functions.lua" -pattern = "local four_fingers = next(find_joker('Four Fingers'))" -position = 'at' -payload = "local four_fingers = SMODS.four_fingers('flush')" -match_indent = true - -[[patches]] -[patches.pattern] -target = "functions/misc_functions.lua" -pattern = "if #hand > 5 or #hand < (5 - (four_fingers and 1 or 0)) then return ret else" -position = 'at' -payload = "if #hand < four_fingers then return ret else" -match_indent = true - -[[patches]] -[patches.pattern] -target = "functions/misc_functions.lua" -pattern = "if flush_count >= (5 - (four_fingers and 1 or 0)) then" -position = 'at' -payload = "if flush_count >= four_fingers then" -match_indent = true - -# get_X_same() -[[patches]] -[patches.pattern] -target = 'functions/misc_functions.lua' -pattern = 'local vals = {{},{},{},{},{},{},{},{},{},{},{},{},{},{}}' -position = 'at' -match_indent = true -payload = ''' -local vals = {} -for i = 1, SMODS.Rank.max_id.value do - vals[i] = {} -end''' - -[[patches]] -[patches.pattern] -target = 'functions/misc_functions.lua' -pattern = 'function get_X_same(num, hand)' -position = 'at' -match_indent = true -payload = '''function get_X_same(num, hand, or_more)''' - -[[patches]] -[patches.pattern] -target = 'functions/misc_functions.lua' -pattern = 'if #curr == num then' -position = 'at' -match_indent = true -payload = '''if or_more and (#curr >= num) or (#curr == num) then''' - # Card:get_nominal() [[patches]] [patches.regex] diff --git a/lovely/quantum_card_fields.toml b/lovely/quantum_card_fields.toml new file mode 100644 index 000000000..8e82b9257 --- /dev/null +++ b/lovely/quantum_card_fields.toml @@ -0,0 +1,344 @@ +[manifest] +version = "1.0.0" +dump_lua = true +priority = -10 + + +### Seal ### + +# functions/state_events.lua : G.FUNCS.discard_cards_from_highlighted() : Remove (now) duplicate Seal calc +[[patches]] +[patches.pattern] +target = "functions/state_events.lua" +pattern = '''G.hand.highlighted[i]:calculate_seal({discard = true})''' +position = "at" +match_indent = true +payload = '''''' + + +### Suit ### + +# blind.lua : Blind:debuff_card() : update card:is_suit() check and add card:is_rank() check +[[patches]] +[patches.pattern] +target = "blind.lua" +pattern = '''if self.debuff.suit and card:is_suit(self.debuff.suit, true) then''' +position = "at" +match_indent = true +payload = '''if self.debuff.suit and card:is_suit(self.debuff.suit, {bypass_debuff = true}) then''' +[[patches]] +[patches.pattern] +target = "blind.lua" +pattern = '''if self.debuff.value and self.debuff.value == card.base.value then''' +position = "at" +match_indent = true +payload = '''if self.debuff.value and card:is_rank(self.debuff.value) then''' + +# Blackboard +[[patches]] +[patches.pattern] +target = "card.lua" +pattern = '''if v:is_suit('Clubs', nil, true) or v:is_suit('Spades', nil, true) then''' +position = "at" +match_indent = true +payload = '''if v:is_suits({Clubs = true, Spades = true}) then''' + +### Rank ### + +# Oh boy. + +# Help +# [[patches]] +# [patches.regex] +# target = "functions/common_events.lua" +# pattern = ''':get_id\(\)\s+==\s+([^\)]*?)\s(and|then)''' +# position = 'at' +# payload = ''':is_rank(SMODS.get_rank_from_id($1)) $2''' + +# Sixth Sense +[[patches]] +[patches.pattern] +target = "card.lua" +pattern = ''' +if self.ability.name == 'Sixth Sense' and #context.full_hand == 1 and context.full_hand[1]:get_id() == 6 and not context.full_hand[1].sixth_sense and G.GAME.current_round.hands_played == 0 then''' +position = "at" +match_indent = true +payload = ''' +if self.ability.name == 'Sixth Sense' and #context.full_hand == 1 and context.full_hand[1]:is_rank("6") and not context.full_hand[1].sixth_sense and G.GAME.current_round.hands_played == 0 then''' + +# Mail-In Rebate +[[patches]] +[patches.pattern] +target = "card.lua" +pattern = ''' + context.other_card:get_id() == G.GAME.current_round.mail_card.id then''' +position = "at" +match_indent = false +payload = ''' + context.other_card:is_rank(G.GAME.current_round.mail_card.rank) then''' + +# Hit the Road +[[patches]] +[patches.pattern] +target = "card.lua" +pattern = '''context.other_card:get_id() == 11 and not context.blueprint then''' +position = "at" +match_indent = true +payload = '''context.other_card:is_rank("Jack") and not context.blueprint then''' + +# Wee Joker +[[patches]] +[patches.pattern] +target = "card.lua" +pattern = '''context.other_card:get_id() == 2 and not context.blueprint then''' +position = "at" +match_indent = true +payload = '''context.other_card:is_rank("2") and not context.blueprint then''' + +# 8 Ball +[[patches]] +[patches.pattern] +target = "card.lua" +pattern = ''' + if (context.other_card:get_id() == 8) and (pseudorandom('8ball') < G.GAME.probabilities.normal/self.ability.extra) then''' # This gets injected before listed_probabilities.toml +position = "at" +match_indent = false +payload = ''' + if (context.other_card:is_rank("8")) and (pseudorandom('8ball') < G.GAME.probabilities.normal/self.ability.extra) then''' + +# The Idol +[[patches]] +[patches.pattern] +target = "card.lua" +pattern = ''' + context.other_card:get_id() == G.GAME.current_round.idol_card.id and ''' +position = "at" +match_indent = false +payload = ''' + context.other_card:is_rank(G.GAME.current_round.idol_card.rank) and ''' + +# Scholar +[[patches]] +[patches.pattern] +target = "card.lua" +pattern = ''' + context.other_card:get_id() == 14 then''' +position = "at" +match_indent = false +payload = ''' + context.other_card:is_rank("Ace") then''' + +# Walkie Talkie +[[patches]] +[patches.pattern] +target = "card.lua" +pattern = ''' + (context.other_card:get_id() == 10 or context.other_card:get_id() == 4) then''' +position = "at" +match_indent = false +payload = ''' + (context.other_card:is_ranks({["10"]=true, ["4"]=true})) then''' + +# Fibonacci +[[patches]] +[patches.pattern] +target = "card.lua" +pattern = ''' +if self.ability.name == 'Fibonacci' and ( + context.other_card:get_id() == 2 or + context.other_card:get_id() == 3 or + context.other_card:get_id() == 5 or + context.other_card:get_id() == 8 or + context.other_card:get_id() == 14) then + return { + mult = self.ability.extra, + card = self + } + end +''' +position = "at" +match_indent = true +payload = ''' +if self.ability.name == 'Fibonacci' and ( + context.other_card:is_ranks({["2"]=true, ["3"]=true, ["5"]=true, ["8"]=true, ["Ace"]=true})) then + return { + mult = self.ability.extra, + card = self + } + end +''' + +# Triboulet +[[patches]] +[patches.pattern] +target = "card.lua" +pattern = ''' + (context.other_card:get_id() == 12 or context.other_card:get_id() == 13) then''' +position = "at" +match_indent = false +payload = ''' + (context.other_card:is_ranks({["Queen"]=true, ["King"]=true})) then''' + +# Shoot the Moon +[[patches]] +[patches.pattern] +target = "card.lua" +pattern = ''' + context.other_card:get_id() == 12 then''' +position = "at" +match_indent = false +payload = ''' + context.other_card:is_rank("Queen") then''' + +# Baron +[[patches]] +[patches.pattern] +target = "card.lua" +pattern = ''' + context.other_card:get_id() == 13 then''' +position = "at" +match_indent = false +payload = ''' + context.other_card:is_rank("King") then''' + +# Hack +[[patches]] +[patches.pattern] +target = "card.lua" +pattern = ''' +if self.ability.name == 'Hack' and ( + context.other_card:get_id() == 2 or + context.other_card:get_id() == 3 or + context.other_card:get_id() == 4 or + context.other_card:get_id() == 5) then + return { + message = localize('k_again_ex'), + repetitions = self.ability.extra, + card = self + } + end +''' +position = "at" +match_indent = true +payload = ''' +if self.ability.name == 'Hack' and ( + context.other_card:is_ranks({["2"]=true, ["3"]=true, ["4"]=true, ["5"]=true})) then + return { + message = localize('k_again_ex'), + repetitions = self.ability.extra, + card = self + } + end +''' + +# Superposition +[[patches]] +[patches.pattern] +target = "card.lua" +pattern = ''' + if context.scoring_hand[i]:get_id() == 14 then aces = aces + 1 end''' +position = "at" +match_indent = false +payload = ''' + if context.scoring_hand[i]:is_rank("Ace") then aces = aces + 1 end''' + +# Even Steven and Odd Todd +[[patches]] +[patches.pattern] +target = "card.lua" +pattern = ''' +if self.ability.name == 'Even Steven' and +context.other_card:get_id() <= 10 and +context.other_card:get_id() >= 0 and +context.other_card:get_id()%2 == 0 +then + return { + mult = self.ability.extra, + card = self + } +end +if self.ability.name == 'Odd Todd' and +((context.other_card:get_id() <= 10 and +context.other_card:get_id() >= 0 and +context.other_card:get_id()%2 == 1) or +(context.other_card:get_id() == 14)) +then + return { + chips = self.ability.extra, + card = self + } +end +''' +position = "at" +match_indent = true +payload = ''' +if self.ability.name == 'Even Steven' and +context.other_card:is_parity(0) +then + return { + mult = self.ability.extra, + card = self + } +end +if self.ability.name == 'Odd Todd' and +context.other_card:is_parity(1) +then + return { + chips = self.ability.extra, + card = self + } +end +''' + +### Sticker ### +# card.lua : Card:calculate_perishable() : update logic +[[patches]] +[patches.pattern] +target = "card.lua" +pattern = ''' +if self.ability.perishable and self.ability.perish_tally > 0 then + if self.ability.perish_tally == 1 then + self.ability.perish_tally = 0 + card_eval_status_text(self, 'extra', nil, nil, nil, {message = localize('k_disabled_ex'),colour = G.C.FILTER, delay = 0.45}) + self:set_debuff() + else + self.ability.perish_tally = self.ability.perish_tally - 1 + card_eval_status_text(self, 'extra', nil, nil, nil, {message = localize{type='variable',key='a_remaining',vars={self.ability.perish_tally}},colour = G.C.FILTER, delay = 0.45}) + end +end''' +position = "at" +match_indent = true +payload = ''' +local is_perishable = self:has_sticker("perishable", {bypass_debuff = true}) +if is_perishable and self.ability.perish_tally and self.ability.perish_tally > 0 then + if self.ability.perish_tally == 1 then + self.ability.perish_tally = 0 + card_eval_status_text(self, 'extra', nil, nil, nil, {message = localize('k_disabled_ex'),colour = G.C.FILTER, delay = 0.45}) + self:set_debuff() + else + self.ability.perish_tally = self.ability.perish_tally - 1 + card_eval_status_text(self, 'extra', nil, nil, nil, {message = localize{type='variable',key='a_remaining',vars={self.ability.perish_tally}},colour = G.C.FILTER, delay = 0.45}) + end +elseif is_perishable then + self.ability.perish_tally = 4 + card_eval_status_text(self, 'extra', nil, nil, nil, {message = localize{type='variable',key='a_remaining',vars={self.ability.perish_tally}},colour = G.C.FILTER, delay = 0.45}) +else + self.ability.perish_tally = nil +end''' + +# card.lua : Card:calculate_rental() : update logic +[[patches]] +[patches.pattern] +target = "card.lua" +pattern = ''' +if self.ability.rental then + ease_dollars(-G.GAME.rental_rate) + card_eval_status_text(self, 'dollars', -G.GAME.rental_rate) +end''' +position = "at" +match_indent = true +payload = ''' +if self:has_sticker("rental", {bypass_debuff = true}) then + ease_dollars(-G.GAME.rental_rate) + card_eval_status_text(self, 'dollars', -G.GAME.rental_rate) +end''' \ No newline at end of file diff --git a/lovely/seal.toml b/lovely/seal.toml index c227e00f0..075714bf5 100644 --- a/lovely/seal.toml +++ b/lovely/seal.toml @@ -36,31 +36,16 @@ for k, v in pairs(SMODS.Seals) do G.BADGE_COL[k:lower()..'_seal'] = v.badge_colour end''' -# Card:calculate_seal() -[[patches]] -[patches.regex] -target = "card.lua" -pattern = 'function Card:calculate_seal\(context\)\n(?[\t ]*)if self.debuff then return nil end' -position = 'after' -line_prepend = '$indent' -payload = ''' - - -local obj = G.P_SEALS[self.seal] or {} -if obj.calculate and type(obj.calculate) == 'function' then - local o, t = obj:calculate(self, context) - if o then - if not o.card then o.card = self end - end - if o or t then return o, t end -end''' - # Card:update() [[patches]] [patches.pattern] target = "card.lua" -pattern = 'if G.STAGE == G.STAGES.RUN then' -position = 'before' +pattern = ''' + if self.ability.consumeable and self.ability.consumeable.max_highlighted then + self.ability.consumeable.mod_num = math.min(5, self.ability.consumeable.max_highlighted) + end +''' +position = 'after' match_indent = true payload = ''' local obj = G.P_SEALS[self.seal] or {} @@ -68,24 +53,6 @@ if obj.update and type(obj.update) == 'function' then obj:update(self, dt) end''' -# Card:get_p_dollars() -# Also, Gold Seal respects quantum enhancements -# Patch is here to avoid conflict -[[patches]] -[patches.regex] -target = "card.lua" -pattern = '''(?[\t ]*)if self\.seal == 'Gold' then''' -position = 'at' -line_prepend = '$indent' -payload = ''' -local obj = G.P_SEALS[self.seal] or {} -if obj.get_p_dollars and type(obj.get_p_dollars) == 'function' then - ret = ret + obj:get_p_dollars(self) -elseif self.seal == 'Gold' and not self.ability.extra_enhancement then''' -# note for later: the Card:get_xxx functions sometimes take context and sometimes don't, -# which is annoying for enhancements -# this should probably be changed to be consistent in better calc - # generate_card_ui() [[patches]] [patches.pattern] @@ -169,43 +136,6 @@ Purple = {order = 4, discovered = false, set = "Seal"}, } ''' -# Card:set_seal() -[[patches]] -[patches.pattern] -target = 'card.lua' -pattern = '''G.CONTROLLER.locks.seal = true''' -position = 'after' -match_indent = true -payload = '''local sound = G.P_SEALS[_seal].sound or {sound = 'gold_seal', per = 1.2, vol = 0.4}''' -[[patches]] -[patches.pattern] -target = 'card.lua' -pattern = '''play_sound('gold_seal', 1.2, 0.4)''' -position = 'at' -match_indent = true -payload = ''' -self.ability.delay_seal = false -play_sound(sound.sound, sound.per, sound.vol)''' -## Populate Seal Ability Table -[[patches]] -[patches.pattern] -target = 'card.lua' -pattern = '''self.seal = _seal''' -position = 'after' -match_indent = true -payload = ''' -self.ability.seal = {} -for k, v in pairs(G.P_SEALS[_seal].config or {}) do - if type(v) == 'table' then - self.ability.seal[k] = copy_table(v) - else - self.ability.seal[k] = v - end -end - -self.ability.delay_seal = not silent -''' - # card_limit support [[patches]] [patches.pattern] diff --git a/lsp_def/classes/edition.lua b/lsp_def/classes/edition.lua index 9013b70ec..3aa727653 100644 --- a/lsp_def/classes/edition.lua +++ b/lsp_def/classes/edition.lua @@ -46,12 +46,6 @@ SMODS.Edition = setmetatable({}, { end }) ----@param self Card|table ----@param context CalcContext|table ----@return table? ---- Calculates Editions on cards. -function Card:calculate_edition(context) end - ---@param self Card|table ---@param edition? Editions|string|{[string]: true} Both `string` values are the key of the edition to apply. ---@param immediate? boolean diff --git a/lsp_def/classes/enhancement.lua b/lsp_def/classes/enhancement.lua index 1b58ad900..d91715724 100644 --- a/lsp_def/classes/enhancement.lua +++ b/lsp_def/classes/enhancement.lua @@ -8,6 +8,7 @@ ---@field no_rank? boolean Enhanced cards have no rank ---@field no_suit? boolean Enhanced cards have no suit. ---@field any_suit? boolean Enhanced cards have all suits. +---@field any_rank? boolean Enhanced cards have all ranks. (Requires SMODS.optional_features.quantum_fields.rank = true) ---@field overrides_base_rank? boolean Enhancement cannot be generated by consumables. Automatically set to `true` if Enhancement has `no_rank`. ---@field always_scores? boolean Enhanced cards always score. ---@field weight? number Weight of the enhancement. @@ -33,12 +34,6 @@ SMODS.Enhancement = setmetatable({}, { end }) ----@param self Card|table ----@param context CalcContext|table ----@return table? ---- Calculates Enhancements on cards. -function Card:calculate_enhancement(context) end - ---@param args table|{key?: string, type_key?: string, mod?: number, guaranteed?: true, options?: table} ---@return Enhancements|string? --- Polls all Enhancements with `args` for additional settings, and returns the key to a selected enhancement. diff --git a/lsp_def/classes/quantum_card_field.lua b/lsp_def/classes/quantum_card_field.lua new file mode 100644 index 000000000..f1d72fc79 --- /dev/null +++ b/lsp_def/classes/quantum_card_field.lua @@ -0,0 +1,784 @@ +---@meta + +---@class SMODS.QuantumCardField: SMODS.GameObject +---@field obj_buffer? QuantumCardFields|string[] Array of keys to all objects registered to this class. +---@field obj_table? table Table of objects registered to this class. +---@field loc_txt? table|{name: string} Contains strings used for displaying text related to this object. +---@field super? SMODS.GameObject|table Parent class. +---@field __call? fun(self: SMODS.QuantumCardField|table, o: SMODS.QuantumCardField|table): nil|table|SMODS.QuantumCardField +---@field extend? fun(self: SMODS.QuantumCardField|table, o: SMODS.QuantumCardField|table): table Primary method of creating a class. +---@field check_duplicate_register? fun(self: SMODS.QuantumCardField|table): boolean? Ensures objects already registered will not register. +---@field check_duplicate_key? fun(self: SMODS.QuantumCardField|table): boolean? Ensures objects with duplicate keys will not register. Checked on `__call` but not `take_ownership`. For take_ownership, the key must exist. +---@field register? fun(self: SMODS.QuantumCardField|table) Registers the object. +---@field check_dependencies? fun(self: SMODS.QuantumCardField|table): boolean? Returns `true` if there's no failed dependencies. +---@field process_loc_text? fun(self: SMODS.QuantumCardField|table) Called during `inject_class`. Handles injecting loc_text. +---@field send_to_subclasses? fun(self: SMODS.QuantumCardField|table, func: string, ...: any) Starting from this class, recusively searches for functions with the given key on all subordinate classes and run all found functions with the given arguments. +---@field pre_inject_class? fun(self: SMODS.QuantumCardField|table) Called before `inject_class`. Injects and manages class information before object injection. +---@field post_inject_class? fun(self: SMODS.QuantumCardField|table) Called after `inject_class`. Injects and manages class information after object injection. +---@field inject_class? fun(self: SMODS.QuantumCardField|table) Injects all direct instances of class objects by calling `obj:inject` and `obj:process_loc_text`. Also injects anything necessary for the class itself. Only called if class has defined both `obj_table` and `obj_buffer`. +---@field inject? fun(self: SMODS.QuantumCardField|table, i?: number) Called during `inject_class`. Injects the object into the game. +---@field take_ownership? fun(self: SMODS.QuantumCardField|table, key: string, obj: SMODS.QuantumCardField|table, silent?: boolean): nil|table|SMODS.QuantumCardField Takes control of vanilla objects. Child class must have get_obj for this to function +---@field get_obj? fun(self: SMODS.QuantumCardField|table, key: string): SMODS.QuantumCardField|table? Returns an object if one matches the `key`. +---@field loc_vars? fun(self: SMODS.QuantumCardField|table, info_queue: table, card: Card|table) Allows adding tooltips onto cards with this suit. Return values not respected. +---@field delete? fun(self: SMODS.QuantumCardField|table) Deletes this suit. +---@field default_enabled? boolean Whether this QField is enabled by default. +---@field g_obj_table? table The map to this QField's objects, e.g. SMODS.Ranks. +---@field get_context_flag? string The flag to use in the getter context, e.g. "get_ranks" or "check_enhancement". +---@field has_context_flag? string The flag to use un the card_has_check context, e.g. "has_rank" or "has_enhancement". +---@field return_flag? string The flag used in calculate returns, e.g "ranks" or "enhancements" = [table map of values] +---@field calc_key? string The key this QField injects its calculation returns into the return_table, e.g. 'edition' -> `ret.edition` (see vanilla eval_card()) +---@field cache_ability? boolean Whether this QField should cache obj.ability; This allows e.g. Quantum Enhancements to add to Card:get_chip_mult() etc. +---@field base_value_ref? string A table subfield path string to get the base value, e.g. "base.value" for Rank, "config.center.key" for Enhancement. +---@field get_base_value? fun(self: SMODS.QuantumCardField|table, card: Card): string Returns the value according to the above ^ +---@field base_getter? fun(self: SMODS.QuantumCardField|table, card: Card, args: table|nil): table Returns a map of the base QField values for a Card before has_no/has_any or any quantum effects. +---@overload fun(self: SMODS.QuantumCardField): SMODS.QuantumCardField +SMODS.QuantumCardField = setmetatable({}, { + __call = function(self) + return self + end +}) + +---@type table +SMODS.QuantumCardFields = {} + +---@param card Card Gets and sets SMODS.qfield_cache[card], which has the subfields .get, .has and (maybe) .abilities; +-- .get[qfield.return_flag] = [table map of values] +-- .has[qfield.key] = {any = [boolean?], no = [boolean?]} +-- (if cached) .abilities = [list of structs:] {{t = [ability table], key = [obj.key that cached the ability], qfield_key = [qfield the obj belongs to]}, ...} +---@return table cache The cached values, {has = ..., get = ...} ^ see above. +function SMODS.set_quantum_cache(card) end + +------------------------------- +----- Rank +------------------------------- + +--- ! Injected by QuantumCardField.inject +---@param args table|nil Args table, used flags: [as_objs: Whether to return objs or obj keys.] +---@return table get SMODS.qfield_cache[card].get[qfield.return_flag] +function Card:get_ranks(args) end + +--- ! Injected by QuantumCardField.inject +---@param card Card The card to check. +---@param ... ... Passed to getter func. +---@return boolean any SMODS.qfield_cache[card].has[qfield.key].any +function SMODS.has_any_rank(card, ...) end + +--- ! Injected by QuantumCardField.inject +---@param card Card The card to check. +---@param ... ... Passed to getter func. +---@return boolean no SMODS.qfield_cache[card].has[qfield.key].no and not SMODS.qfield_cache[card].has[qfield.key].any +function SMODS.has_no_rank(card, ...) end + +--- ! Injected by QuantumCardField.inject +--- Calls plural_is func (see below) +---@param value string The obj.key to check, e.g. "Ace" or "2" +---@param args table|nil Passed to plural_is func and getter func. +---@param ... ... Passed to getter func. +---@return boolean is +function Card:is_rank(value, args, ...) end + +--- ! Injected by QuantumCardField.inject +---@param value_map string A map of `obj.key`s to check against. +---@param args table|nil Passed to getter func and uses flags: [bypass_debuff: Whether to ignore card.debuff, all: Whether all values of the value_map must match (or just any)] +---@param ... ... Passed to getter func. +---@return boolean is +function Card:is_ranks(value_map, args, ...) end + +--- ! Injected by QuantumCardField.inject +---@param cards table A list of cards. +---@param ... ... Passed to getter func. +---@return table tally A table map mapping a QField value to its tally, e.g. `{m_stone = 2}`. +---@return table> value_to_cards A table map mapping a QField value to a table map of cards. +function SMODS.get_rank_tally(cards, ...) end + +--- ! Injected by QuantumCardField.inject +---@param context CalcContext The context. +---@param ... ... Passed to getter func. +---@return table ret Effects table. +function Card:calculate_rank(context, ...) end + +------------------------------- +----- Enhancement +------------------------------- + +--- ! Injected by QuantumCardField.inject +---@param args table|nil Args table, used flags: [as_objs: Whether to return objs or obj keys.] +---@return table get SMODS.qfield_cache[card].get[qfield.return_flag] +function Card:get_enhancements(args) end + +--- ! Injected by QuantumCardField.inject +--- Redirects to Card:set_ability() +---@param center string See Card:set_ability() +---@param initial boolean|nil See Card:set_ability() +---@param delay_sprites boolean|nil See Card:set_ability() +function Card:set_enhancement(center, initial, delay_sprites) end + +--- ! Injected by QuantumCardField.inject +--- Both Card and SMODS are inject targets via `target_objects = {getter = {Card, SMODS}, is_funcs = {Card, SMODS}}` in obj definition. +---@param card Card The card to get the values for. +---@param args table|nil Args table, used flags: [as_objs: Whether to return objs or obj keys.] +---@return table get SMODS.qfield_cache[card].get[qfield.return_flag] +function SMODS.get_enhancements(card, args) end + +--- ! Injected by QuantumCardField.inject +---@param card Card The card to check. +---@param ... ... Passed to getter func. +---@return boolean any SMODS.qfield_cache[card].has[qfield.key].any +function SMODS.has_any_enhancement(card, ...) end + +--- ! Injected by QuantumCardField.inject +---@param card Card The card to check. +---@param ... ... Passed to getter func. +---@return boolean no SMODS.qfield_cache[card].has[qfield.key].no and not SMODS.qfield_cache[card].has[qfield.key].any +function SMODS.has_no_enhancement(card, ...) end + +--- ! Injected by QuantumCardField.inject +--- `**has**_enhancement` instead of `**is**_enhancement` via `inject_args = {is_func_prefix = "has"}` in obj definition. +--- Calls plural_is func (see below) +---@param value string The obj.key to check, e.g. "m_stone" +---@param args table|nil Passed to plural_is func and getter func. +---@param ... ... Passed to getter func. +---@return boolean is +function Card:has_enhancement(value, args, ...) end + +--- ! Injected by QuantumCardField.inject +--- Both Card and SMODS are inject targets via `target_objects = {getter = {Card, SMODS}, is_funcs = {Card, SMODS}}` in obj definition. +--- `**has**_enhancement` instead of `**is**_enhancement` via `inject_args = {is_func_prefix = "has"}` in obj definition. +--- Calls plural_is func (see below) +---@param card Card The card to check. +---@param value string The obj.key to check, e.g. "m_stone" +---@param args table|nil Passed to plural_is func and getter func. +---@param ... ... Passed to getter func. +---@return boolean is +function SMODS.has_enhancement(card, value, args, ...) end + +--- ! Injected by QuantumCardField.inject +---@param value_map string A map of `obj.key`s to check against. +---@param args table|nil Passed to getter func and uses flags: [bypass_debuff: Whether to ignore card.debuff, all: Whether all values of the value_map must match (or just any)] +---@param ... ... Passed to getter func. +---@return boolean is +function Card:has_enhancements(value_map, args, ...) end + +--- ! Injected by QuantumCardField.inject +--- Both Card and SMODS are inject targets via `target_objects = {getter = {Card, SMODS}, is_funcs = {Card, SMODS}}` in obj definition. +--- `**has**_enhancement` instead of `**is**_enhancement` via `inject_args = {is_func_prefix = "has"}` in obj definition. +---@param card Card The card to check. +---@param value_map string A map of `obj.key`s to check against. +---@param args table|nil Passed to getter func and uses flags: [bypass_debuff: Whether to ignore card.debuff, all: Whether all values of the value_map must match (or just any)] +---@param ... ... Passed to getter func. +---@return boolean is +function SMODS.has_enhancements(card, value_map, args, ...) end + +--- ! Injected by QuantumCardField.inject +---@param cards table A list of cards. +---@param ... ... Passed to getter func. +---@return table tally A table map mapping a QField value to its tally, e.g. `{m_stone = 2}`. +---@return table> value_to_cards A table map mapping a QField value to a table map of cards. +function SMODS.get_enhancement_tally(cards, ...) end + +--- ! Injected by QuantumCardField.inject +---@param context CalcContext The context. +---@param ... ... Passed to getter func. +---@return table ret Effects table. +function Card:calculate_enhancement(context, ...) end + +------------------------------- +----- Seal +------------------------------- + +--- ! Injected by QuantumCardField.inject +---@param args table|nil Args table, used flags: [as_objs: Whether to return objs or obj keys.] +---@return table get SMODS.qfield_cache[card].get[qfield.return_flag] +function Card:get_seals(args) end + +--- ! Injected by QuantumCardField.inject +---@param value string|nil Key of the seal to set, or nil to remove the card's seal. +---@param args table|boolean|nil Args table, used flags: [immediate: whether to set it in an event, silent: whether to play sounds AND whether it is instant (overrules immediate), delay: ?] +---@param immediate table|nil ^ legacy compat +function Card:set_seal(value, args, immediate) end + +--- ! Injected by QuantumCardField.inject +---@param card Card The card to check. +---@param ... ... Passed to getter func. +---@return boolean any SMODS.qfield_cache[card].has[qfield.key].any +function SMODS.has_any_seal(card, ...) end + +--- ! Injected by QuantumCardField.inject +---@param card Card The card to check. +---@param ... ... Passed to getter func. +---@return boolean no SMODS.qfield_cache[card].has[qfield.key].no and not SMODS.qfield_cache[card].has[qfield.key].any +function SMODS.has_no_seal(card, ...) end + +--- ! Injected by QuantumCardField.inject +--- `**has**_seal` instead of `**is**_seal` via `inject_args = {is_func_prefix = "has"}` in obj definition. +--- Calls plural_is func (see below) +---@param value string The obj.key to check, e.g. "Red" +---@param args table|nil Passed to plural_is func and getter func. +---@param ... ... Passed to getter func. +---@return boolean is +function Card:has_seal(value, args, ...) end + +--- ! Injected by QuantumCardField.inject +--- `**has**_seal` instead of `**is**_seal` via `inject_args = {is_func_prefix = "has"}` in obj definition. +---@param value_map string A map of `obj.key`s to check against. +---@param args table|nil Passed to getter func and uses flags: [bypass_debuff: Whether to ignore card.debuff, all: Whether all values of the value_map must match (or just any)] +---@param ... ... Passed to getter func. +---@return boolean is +function Card:has_seals(value_map, args, ...) end + +--- ! Injected by QuantumCardField.inject +---@param cards table A list of cards. +---@param ... ... Passed to getter func. +---@return table tally A table map mapping a QField value to its tally, e.g. `{m_stone = 2}`. +---@return table> value_to_cards A table map mapping a QField value to a table map of cards. +function SMODS.get_seal_tally(cards, ...) end + +--- ! Injected by QuantumCardField.inject +---@param context CalcContext The context. +---@param ... ... Passed to getter func. +---@return table ret Effects table. +function Card:calculate_seal(context, ...) end + +------------------------------- +----- Edition +------------------------------- + +--- ! Injected by QuantumCardField.inject +---@param args table|nil Args table, used flags: [as_objs: Whether to return objs or obj keys.] +---@return table get SMODS.qfield_cache[card].get[qfield.return_flag] +function Card:get_editions(args) end + +--- ! Injected by QuantumCardField.inject +---@param value string|nil Key of the Edition to set, or nil to remove the card's Edition. +---@param args table|boolean|nil Args table, used flags: [immediate: whether to set it in an event, silent: whether to play sounds AND whether it is instant (overrules immediate), delay: ?] +---@param silent boolean|nil ^ legacy compat +---@param delay boolean|nil ^ legacy compat +function Card:set_edition(value, args, silent, delay) end + +--- ! Injected by QuantumCardField.inject +---@param card Card The card to check. +---@param ... ... Passed to getter func. +---@return boolean any SMODS.qfield_cache[card].has[qfield.key].any +function SMODS.has_any_edition(card, ...) end + +--- ! Injected by QuantumCardField.inject +---@param card Card The card to check. +---@param ... ... Passed to getter func. +---@return boolean no SMODS.qfield_cache[card].has[qfield.key].no and not SMODS.qfield_cache[card].has[qfield.key].any +function SMODS.has_no_edition(card, ...) end + +--- ! Injected by QuantumCardField.inject +--- Calls plural_is func (see below) +---@param value string The obj.key to check, e.g. "e_holo" +---@param args table|nil Passed to plural_is func and getter func. +---@param ... ... Passed to getter func. +---@return boolean is +function Card:is_edition(value, args, ...) end + +--- ! Injected by QuantumCardField.inject +---@param value_map string A map of `obj.key`s to check against. +---@param args table|nil Passed to getter func and uses flags: [bypass_debuff: Whether to ignore card.debuff, all: Whether all values of the value_map must match (or just any)] +---@param ... ... Passed to getter func. +---@return boolean is +function Card:is_editions(value_map, args, ...) end + +--- ! Injected by QuantumCardField.inject +---@param cards table A list of cards. +---@param ... ... Passed to getter func. +---@return table tally A table map mapping a QField value to its tally, e.g. `{m_stone = 2}`. +---@return table> value_to_cards A table map mapping a QField value to a table map of cards. +function SMODS.get_edition_tally(cards, ...) end + +--- ! Injected by QuantumCardField.inject +---@param context CalcContext The context. +---@param ... ... Passed to getter func. +---@return table ret Effects table. +function Card:calculate_edition(context, ...) end + +------------------------------- +----- Suit +------------------------------- + +--- ! Injected by QuantumCardField.inject +---@param args table|nil Args table, used flags: [as_objs: Whether to return objs or obj keys.] +---@return table get SMODS.qfield_cache[card].get[qfield.return_flag] +function Card:get_suits(args) end + +--- ! Injected by QuantumCardField.inject +---@param card Card The card to check. +---@param ... ... Passed to getter func. +---@return boolean any SMODS.qfield_cache[card].has[qfield.key].any +function SMODS.has_any_suit(card, ...) end + +--- ! Injected by QuantumCardField.inject +---@param card Card The card to check. +---@param ... ... Passed to getter func. +---@return boolean no SMODS.qfield_cache[card].has[qfield.key].no and not SMODS.qfield_cache[card].has[qfield.key].any +function SMODS.has_no_suit(card, ...) end + +--- ! Injected by QuantumCardField.inject +--- Calls plural_is func (see below) +---@param value string The obj.key to check, e.g. "Spades" +---@param args table|nil Passed to plural_is func and getter func. +---@param ... ... Passed to getter func. +---@return boolean is +function Card:is_suit(value, args, ...) end + +--- ! Injected by QuantumCardField.inject +---@param value_map string A map of `obj.key`s to check against. +---@param args table|nil Passed to getter func and uses flags: [bypass_debuff: Whether to ignore card.debuff, all: Whether all values of the value_map must match (or just any)] +---@param ... ... Passed to getter func. +---@return boolean is +function Card:is_suits(value_map, args, ...) end + +--- ! Injected by QuantumCardField.inject +---@param cards table A list of cards. +---@param ... ... Passed to getter func. +---@return table tally A table map mapping a QField value to its tally, e.g. `{m_stone = 2}`. +---@return table> value_to_cards A table map mapping a QField value to a table map of cards. +function SMODS.get_suit_tally(cards, ...) end + +--- ! Injected by QuantumCardField.inject +---@param context CalcContext The context. +---@param ... ... Passed to getter func. +---@return table ret Effects table. +function Card:calculate_suit(context, ...) end + +------------------------------- +----- Sticker +------------------------------- + +--- ! Injected by QuantumCardField.inject +---@param args table|nil Args table, used flags: [as_objs: Whether to return objs or obj keys.] +---@return table get SMODS.qfield_cache[card].get[qfield.return_flag] +function Card:get_stickers(args) end + +--- ! Injected by QuantumCardField.inject +---@param card Card The card to check. +---@param ... ... Passed to getter func. +---@return boolean any SMODS.qfield_cache[card].has[qfield.key].any +function SMODS.has_any_sticker(card, ...) end + +--- ! Injected by QuantumCardField.inject +---@param card Card The card to check. +---@param ... ... Passed to getter func. +---@return boolean no SMODS.qfield_cache[card].has[qfield.key].no and not SMODS.qfield_cache[card].has[qfield.key].any +function SMODS.has_no_sticker(card, ...) end + +--- ! Injected by QuantumCardField.inject +--- `**has**_sticker` instead of `**is**_sticker` via `inject_args = {is_func_prefix = "has"}` in obj definition. +--- Calls plural_is func (see below) +---@param value string The obj.key to check, e.g. "perishable" +---@param args table|nil Passed to plural_is func and getter func. +---@param ... ... Passed to getter func. +---@return boolean is +function Card:has_sticker(value, args, ...) end + +--- ! Injected by QuantumCardField.inject +--- `**has**_sticker` instead of `**is**_sticker` via `inject_args = {is_func_prefix = "has"}` in obj definition. +---@param value_map string A map of `obj.key`s to check against. +---@param args table|nil Passed to getter func and uses flags: [bypass_debuff: Whether to ignore card.debuff, all: Whether all values of the value_map must match (or just any)] +---@param ... ... Passed to getter func. +---@return boolean is +function Card:has_stickers(value_map, args, ...) end + +--- ! Injected by QuantumCardField.inject +---@param cards table A list of cards. +---@param ... ... Passed to getter func. +---@return table tally A table map mapping a QField value to its tally, e.g. `{m_stone = 2}`. +---@return table> value_to_cards A table map mapping a QField value to a table map of cards. +function SMODS.get_sticker_tally(cards, ...) end + +--- ! Injected by QuantumCardField.inject +---@param context CalcContext The context. +---@param ... ... Passed to getter func. +---@return table ret Effects table. +function Card:calculate_sticker(context, ...) end +---@meta + +---@class SMODS.QuantumCardField: SMODS.GameObject +---@field obj_buffer? QuantumCardFields|string[] Array of keys to all objects registered to this class. +---@field obj_table? table Table of objects registered to this class. +---@field loc_txt? table|{name: string} Contains strings used for displaying text related to this object. +---@field super? SMODS.GameObject|table Parent class. +---@field __call? fun(self: SMODS.QuantumCardField|table, o: SMODS.QuantumCardField|table): nil|table|SMODS.QuantumCardField +---@field extend? fun(self: SMODS.QuantumCardField|table, o: SMODS.QuantumCardField|table): table Primary method of creating a class. +---@field check_duplicate_register? fun(self: SMODS.QuantumCardField|table): boolean? Ensures objects already registered will not register. +---@field check_duplicate_key? fun(self: SMODS.QuantumCardField|table): boolean? Ensures objects with duplicate keys will not register. Checked on `__call` but not `take_ownership`. For take_ownership, the key must exist. +---@field register? fun(self: SMODS.QuantumCardField|table) Registers the object. +---@field check_dependencies? fun(self: SMODS.QuantumCardField|table): boolean? Returns `true` if there's no failed dependencies. +---@field process_loc_text? fun(self: SMODS.QuantumCardField|table) Called during `inject_class`. Handles injecting loc_text. +---@field send_to_subclasses? fun(self: SMODS.QuantumCardField|table, func: string, ...: any) Starting from this class, recusively searches for functions with the given key on all subordinate classes and run all found functions with the given arguments. +---@field pre_inject_class? fun(self: SMODS.QuantumCardField|table) Called before `inject_class`. Injects and manages class information before object injection. +---@field post_inject_class? fun(self: SMODS.QuantumCardField|table) Called after `inject_class`. Injects and manages class information after object injection. +---@field inject_class? fun(self: SMODS.QuantumCardField|table) Injects all direct instances of class objects by calling `obj:inject` and `obj:process_loc_text`. Also injects anything necessary for the class itself. Only called if class has defined both `obj_table` and `obj_buffer`. +---@field inject? fun(self: SMODS.QuantumCardField|table, i?: number) Called during `inject_class`. Injects the object into the game. +---@field take_ownership? fun(self: SMODS.QuantumCardField|table, key: string, obj: SMODS.QuantumCardField|table, silent?: boolean): nil|table|SMODS.QuantumCardField Takes control of vanilla objects. Child class must have get_obj for this to function +---@field get_obj? fun(self: SMODS.QuantumCardField|table, key: string): SMODS.QuantumCardField|table? Returns an object if one matches the `key`. +---@field loc_vars? fun(self: SMODS.QuantumCardField|table, info_queue: table, card: Card|table) Allows adding tooltips onto cards with this suit. Return values not respected. +---@field delete? fun(self: SMODS.QuantumCardField|table) Deletes this suit. +---@field default_enabled? boolean Whether this QField is enabled by default. +---@field g_obj_table? table The map to this QField's objects, e.g. SMODS.Ranks. +---@field get_context_flag? string The flag to use in the getter context, e.g. "get_ranks" or "check_enhancement". +---@field has_context_flag? string The flag to use un the card_has_check context, e.g. "has_rank" or "has_enhancement". +---@field return_flag? string The flag used in calculate returns, e.g "ranks" or "enhancements" = [table map of values] +---@field calc_key? string The key this QField injects its calculation returns into the return_table, e.g. 'edition' -> `ret.edition` (see vanilla eval_card()) +---@field cache_ability? boolean Whether this QField should cache obj.ability; This allows e.g. Quantum Enhancements to add to Card:get_chip_mult() etc. +---@field base_value_ref? string A table subfield path string to get the base value, e.g. "base.value" for Rank, "config.center.key" for Enhancement. +---@field get_base_value? fun(self: SMODS.QuantumCardField|table, card: Card): string Returns the value according to the above ^ +---@field base_getter? fun(self: SMODS.QuantumCardField|table, card: Card, args: table|nil): table Returns a map of the base QField values for a Card before has_no/has_any or any quantum effects. +---@overload fun(self: SMODS.QuantumCardField): SMODS.QuantumCardField +SMODS.QuantumCardField = setmetatable({}, { + __call = function(self) + return self + end +}) + +---@type table +SMODS.QuantumCardFields = {} + +---@param card Card Gets and sets SMODS.qfield_cache[card], which has the subfields .get, .has and (maybe) .abilities; +-- .get[qfield.return_flag] = [table map of values] +-- .has[qfield.key] = {any = [boolean?], no = [boolean?]} +-- (if cached) .abilities = [list of structs:] {{t = [ability table], key = [obj.key that cached the ability], qfield_key = [qfield the obj belongs to]}, ...} +---@return table cache The cached values, {has = ..., get = ...} ^ see above. +function SMODS.set_quantum_cache(card) end + +------------------------------- +----- Rank +------------------------------- + +--- ! Injected by QuantumCardField.inject +---@param args table|nil Args table, used flags: [as_objs: Whether to return objs or obj keys.] +---@return table get SMODS.qfield_cache[card].get[qfield.return_flag] +function Card:get_ranks(args) end + +--- ! Injected by QuantumCardField.inject +---@param card Card The card to check. +---@param ... ... Passed to getter func. +---@return boolean any SMODS.qfield_cache[card].has[qfield.key].any +function SMODS.has_any_rank(card, ...) end + +--- ! Injected by QuantumCardField.inject +---@param card Card The card to check. +---@param ... ... Passed to getter func. +---@return boolean no SMODS.qfield_cache[card].has[qfield.key].no and not SMODS.qfield_cache[card].has[qfield.key].any +function SMODS.has_no_rank(card, ...) end + +--- ! Injected by QuantumCardField.inject +--- Calls plural_is func (see below) +---@param value string The obj.key to check, e.g. "Ace" or "2" +---@param args table|nil Passed to plural_is func and getter func. +---@param ... ... Passed to getter func. +---@return boolean is +function Card:is_rank(value, args, ...) end + +--- ! Injected by QuantumCardField.inject +---@param value_map string A map of `obj.key`s to check against. +---@param args table|nil Passed to getter func and uses flags: [bypass_debuff: Whether to ignore card.debuff, all: Whether all values of the value_map must match (or just any)] +---@param ... ... Passed to getter func. +---@return boolean is +function Card:is_ranks(value_map, args, ...) end + +--- ! Injected by QuantumCardField.inject +---@param cards table A list of cards. +---@param ... ... Passed to getter func. +---@return table tally A table map mapping a QField value to its tally, e.g. `{m_stone = 2}`. +---@return table> value_to_cards A table map mapping a QField value to a table map of cards. +function SMODS.get_rank_tally(cards, ...) end + +--- ! Injected by QuantumCardField.inject +---@param context CalcContext The context. +---@param ... ... Passed to getter func. +---@return table ret Effects table. +function Card:calculate_rank(context, ...) end + +------------------------------- +----- Enhancement +------------------------------- + +--- ! Injected by QuantumCardField.inject +---@param args table|nil Args table, used flags: [as_objs: Whether to return objs or obj keys.] +---@return table get SMODS.qfield_cache[card].get[qfield.return_flag] +function Card:get_enhancements(args) end + +--- ! Injected by QuantumCardField.inject +--- Redirects to Card:set_ability() +---@param center string See Card:set_ability() +---@param initial? boolean See Card:set_ability() +---@param delay_sprites? boolean See Card:set_ability() +function Card:set_enhancement(center, initial, delay_sprites) end + +--- ! Injected by QuantumCardField.inject +--- Both Card and SMODS are inject targets via `target_objects = {getter = {Card, SMODS}, is_funcs = {Card, SMODS}}` in obj definition. +---@param card Card The card to get the values for. +---@param args table|nil Args table, used flags: [as_objs: Whether to return objs or obj keys.] +---@return table get SMODS.qfield_cache[card].get[qfield.return_flag] +function SMODS.get_enhancements(card, args) end + +--- ! Injected by QuantumCardField.inject +---@param card Card The card to check. +---@param ... ... Passed to getter func. +---@return boolean any SMODS.qfield_cache[card].has[qfield.key].any +function SMODS.has_any_enhancement(card, ...) end + +--- ! Injected by QuantumCardField.inject +---@param card Card The card to check. +---@param ... ... Passed to getter func. +---@return boolean no SMODS.qfield_cache[card].has[qfield.key].no and not SMODS.qfield_cache[card].has[qfield.key].any +function SMODS.has_no_enhancement(card, ...) end + +--- ! Injected by QuantumCardField.inject +--- `**has**_enhancement` instead of `**is**_enhancement` via `inject_args = {is_func_prefix = "has"}` in obj definition. +--- Calls plural_is func (see below) +---@param value string The obj.key to check, e.g. "m_stone" +---@param args table|nil Passed to plural_is func and getter func. +---@param ... ... Passed to getter func. +---@return boolean is +function Card:has_enhancement(value, args, ...) end + +--- ! Injected by QuantumCardField.inject +--- Both Card and SMODS are inject targets via `target_objects = {getter = {Card, SMODS}, is_funcs = {Card, SMODS}}` in obj definition. +--- `**has**_enhancement` instead of `**is**_enhancement` via `inject_args = {is_func_prefix = "has"}` in obj definition. +--- Calls plural_is func (see below) +---@param card Card The card to check. +---@param value string The obj.key to check, e.g. "m_stone" +---@param args table|nil Passed to plural_is func and getter func. +---@param ... ... Passed to getter func. +---@return boolean is +function SMODS.has_enhancement(card, value, args, ...) end + +--- ! Injected by QuantumCardField.inject +---@param value_map string A map of `obj.key`s to check against. +---@param args table|nil Passed to getter func and uses flags: [bypass_debuff: Whether to ignore card.debuff, all: Whether all values of the value_map must match (or just any)] +---@param ... ... Passed to getter func. +---@return boolean is +function Card:has_enhancements(value_map, args, ...) end + +--- ! Injected by QuantumCardField.inject +--- Both Card and SMODS are inject targets via `target_objects = {getter = {Card, SMODS}, is_funcs = {Card, SMODS}}` in obj definition. +--- `**has**_enhancement` instead of `**is**_enhancement` via `inject_args = {is_func_prefix = "has"}` in obj definition. +---@param card Card The card to check. +---@param value_map string A map of `obj.key`s to check against. +---@param args table|nil Passed to getter func and uses flags: [bypass_debuff: Whether to ignore card.debuff, all: Whether all values of the value_map must match (or just any)] +---@param ... ... Passed to getter func. +---@return boolean is +function SMODS.has_enhancements(card, value_map, args, ...) end + +--- ! Injected by QuantumCardField.inject +---@param cards table A list of cards. +---@param ... ... Passed to getter func. +---@return table tally A table map mapping a QField value to its tally, e.g. `{m_stone = 2}`. +---@return table> value_to_cards A table map mapping a QField value to a table map of cards. +function SMODS.get_enhancement_tally(cards, ...) end + +--- ! Injected by QuantumCardField.inject +---@param context CalcContext The context. +---@param ... ... Passed to getter func. +---@return table ret Effects table. +function Card:calculate_enhancement(context, ...) end + +------------------------------- +----- Seal +------------------------------- + +--- ! Injected by QuantumCardField.inject +---@param args table|nil Args table, used flags: [as_objs: Whether to return objs or obj keys.] +---@return table get SMODS.qfield_cache[card].get[qfield.return_flag] +function Card:get_seals(args) end + +--- ! Injected by QuantumCardField.inject +---@param value string Args table, used flags: [as_objs: Whether to return objs or obj keys.] +---@param args? table|nil Args table, used flags: [as_objs: Whether to return objs or obj keys.] +function Card:set_seal(value, args, immediate) end + +--- ! Injected by QuantumCardField.inject +---@param card Card The card to check. +---@param ... ... Passed to getter func. +---@return boolean any SMODS.qfield_cache[card].has[qfield.key].any +function SMODS.has_any_seal(card, ...) end + +--- ! Injected by QuantumCardField.inject +---@param card Card The card to check. +---@param ... ... Passed to getter func. +---@return boolean no SMODS.qfield_cache[card].has[qfield.key].no and not SMODS.qfield_cache[card].has[qfield.key].any +function SMODS.has_no_seal(card, ...) end + +--- ! Injected by QuantumCardField.inject +--- `**has**_seal` instead of `**is**_seal` via `inject_args = {is_func_prefix = "has"}` in obj definition. +--- Calls plural_is func (see below) +---@param value string The obj.key to check, e.g. "Red" +---@param args table|nil Passed to plural_is func and getter func. +---@param ... ... Passed to getter func. +---@return boolean is +function Card:has_seal(value, args, ...) end + +--- ! Injected by QuantumCardField.inject +--- `**has**_seal` instead of `**is**_seal` via `inject_args = {is_func_prefix = "has"}` in obj definition. +---@param value_map string A map of `obj.key`s to check against. +---@param args table|nil Passed to getter func and uses flags: [bypass_debuff: Whether to ignore card.debuff, all: Whether all values of the value_map must match (or just any)] +---@param ... ... Passed to getter func. +---@return boolean is +function Card:has_seals(value_map, args, ...) end + +--- ! Injected by QuantumCardField.inject +---@param cards table A list of cards. +---@param ... ... Passed to getter func. +---@return table tally A table map mapping a QField value to its tally, e.g. `{m_stone = 2}`. +---@return table> value_to_cards A table map mapping a QField value to a table map of cards. +function SMODS.get_seal_tally(cards, ...) end + +--- ! Injected by QuantumCardField.inject +---@param context CalcContext The context. +---@param ... ... Passed to getter func. +---@return table ret Effects table. +function Card:calculate_seal(context, ...) end + +------------------------------- +----- Edition +------------------------------- + +--- ! Injected by QuantumCardField.inject +---@param args table|nil Args table, used flags: [as_objs: Whether to return objs or obj keys.] +---@return table get SMODS.qfield_cache[card].get[qfield.return_flag] +function Card:get_editions(args) end + +--- ! Injected by QuantumCardField.inject +---@param card Card The card to check. +---@param ... ... Passed to getter func. +---@return boolean any SMODS.qfield_cache[card].has[qfield.key].any +function SMODS.has_any_edition(card, ...) end + +--- ! Injected by QuantumCardField.inject +---@param card Card The card to check. +---@param ... ... Passed to getter func. +---@return boolean no SMODS.qfield_cache[card].has[qfield.key].no and not SMODS.qfield_cache[card].has[qfield.key].any +function SMODS.has_no_edition(card, ...) end + +--- ! Injected by QuantumCardField.inject +--- Calls plural_is func (see below) +---@param value string The obj.key to check, e.g. "e_holo" +---@param args table|nil Passed to plural_is func and getter func. +---@param ... ... Passed to getter func. +---@return boolean is +function Card:is_edition(value, args, ...) end + +--- ! Injected by QuantumCardField.inject +---@param value_map string A map of `obj.key`s to check against. +---@param args table|nil Passed to getter func and uses flags: [bypass_debuff: Whether to ignore card.debuff, all: Whether all values of the value_map must match (or just any)] +---@param ... ... Passed to getter func. +---@return boolean is +function Card:is_editions(value_map, args, ...) end + +--- ! Injected by QuantumCardField.inject +---@param cards table A list of cards. +---@param ... ... Passed to getter func. +---@return table tally A table map mapping a QField value to its tally, e.g. `{m_stone = 2}`. +---@return table> value_to_cards A table map mapping a QField value to a table map of cards. +function SMODS.get_edition_tally(cards, ...) end + +--- ! Injected by QuantumCardField.inject +---@param context CalcContext The context. +---@param ... ... Passed to getter func. +---@return table ret Effects table. +function Card:calculate_edition(context, ...) end + +------------------------------- +----- Suit +------------------------------- + +--- ! Injected by QuantumCardField.inject +---@param args table|nil Args table, used flags: [as_objs: Whether to return objs or obj keys.] +---@return table get SMODS.qfield_cache[card].get[qfield.return_flag] +function Card:get_suits(args) end + +--- ! Injected by QuantumCardField.inject +---@param card Card The card to check. +---@param ... ... Passed to getter func. +---@return boolean any SMODS.qfield_cache[card].has[qfield.key].any +function SMODS.has_any_suit(card, ...) end + +--- ! Injected by QuantumCardField.inject +---@param card Card The card to check. +---@param ... ... Passed to getter func. +---@return boolean no SMODS.qfield_cache[card].has[qfield.key].no and not SMODS.qfield_cache[card].has[qfield.key].any +function SMODS.has_no_suit(card, ...) end + +--- ! Injected by QuantumCardField.inject +--- Calls plural_is func (see below) +---@param value string The obj.key to check, e.g. "Spades" +---@param args table|nil Passed to plural_is func and getter func. +---@param ... ... Passed to getter func. +---@return boolean is +function Card:is_suit(value, args, ...) end + +--- ! Injected by QuantumCardField.inject +---@param value_map string A map of `obj.key`s to check against. +---@param args table|nil Passed to getter func and uses flags: [bypass_debuff: Whether to ignore card.debuff, all: Whether all values of the value_map must match (or just any)] +---@param ... ... Passed to getter func. +---@return boolean is +function Card:is_suits(value_map, args, ...) end + +--- ! Injected by QuantumCardField.inject +---@param cards table A list of cards. +---@param ... ... Passed to getter func. +---@return table tally A table map mapping a QField value to its tally, e.g. `{m_stone = 2}`. +---@return table> value_to_cards A table map mapping a QField value to a table map of cards. +function SMODS.get_suit_tally(cards, ...) end + +--- ! Injected by QuantumCardField.inject +---@param context CalcContext The context. +---@param ... ... Passed to getter func. +---@return table ret Effects table. +function Card:calculate_suit(context, ...) end + +------------------------------- +----- Sticker +------------------------------- + +--- ! Injected by QuantumCardField.inject +---@param args table|nil Args table, used flags: [as_objs: Whether to return objs or obj keys.] +---@return table get SMODS.qfield_cache[card].get[qfield.return_flag] +function Card:get_stickers(args) end + +--- ! Injected by QuantumCardField.inject +---@param card Card The card to check. +---@param ... ... Passed to getter func. +---@return boolean any SMODS.qfield_cache[card].has[qfield.key].any +function SMODS.has_any_sticker(card, ...) end + +--- ! Injected by QuantumCardField.inject +---@param card Card The card to check. +---@param ... ... Passed to getter func. +---@return boolean no SMODS.qfield_cache[card].has[qfield.key].no and not SMODS.qfield_cache[card].has[qfield.key].any +function SMODS.has_no_sticker(card, ...) end + +--- ! Injected by QuantumCardField.inject +--- `**has**_sticker` instead of `**is**_sticker` via `inject_args = {is_func_prefix = "has"}` in obj definition. +--- Calls plural_is func (see below) +---@param value string The obj.key to check, e.g. "perishable" +---@param args table|nil Passed to plural_is func and getter func. +---@param ... ... Passed to getter func. +---@return boolean is +function Card:has_sticker(value, args, ...) end + +--- ! Injected by QuantumCardField.inject +--- `**has**_sticker` instead of `**is**_sticker` via `inject_args = {is_func_prefix = "has"}` in obj definition. +---@param value_map string A map of `obj.key`s to check against. +---@param args table|nil Passed to getter func and uses flags: [bypass_debuff: Whether to ignore card.debuff, all: Whether all values of the value_map must match (or just any)] +---@param ... ... Passed to getter func. +---@return boolean is +function Card:has_stickers(value_map, args, ...) end + +--- ! Injected by QuantumCardField.inject +---@param cards table A list of cards. +---@param ... ... Passed to getter func. +---@return table tally A table map mapping a QField value to its tally, e.g. `{m_stone = 2}`. +---@return table> value_to_cards A table map mapping a QField value to a table map of cards. +function SMODS.get_sticker_tally(cards, ...) end + +--- ! Injected by QuantumCardField.inject +---@param context CalcContext The context. +---@param ... ... Passed to getter func. +---@return table ret Effects table. +function Card:calculate_sticker(context, ...) end \ No newline at end of file diff --git a/lsp_def/classes/rank.lua b/lsp_def/classes/rank.lua index 898b63fa7..633e2b716 100644 --- a/lsp_def/classes/rank.lua +++ b/lsp_def/classes/rank.lua @@ -14,10 +14,12 @@ ---@field shorthand? string Short description of this rank in deck preview. ---@field face_nominal? number Determines the displayed order of ranks with the same nominal value. ---@field face? boolean Sets if this rank counts as a "face" card. ----@field next? Ranks[]|string[] List of keys to other ranks that come after this card. ----@field prev? Ranks[]|string[] List of keys to other ranks that come before this card. Used when evaluating straights. ----@field strength_effect? table|{fixed?: number, random?: boolean, ignore?: boolean} Determines how cards with this rank behave when Strength is used. ----@field straight_edge? boolean Sets if this rank behaves like an Ace for straights. +---@field next? Ranks|string[] List of keys to other ranks that come after this card. +---@field prev? Ranks|string[] List of keys to other ranks that come before this card. Used when evaluating straights. +---@field strength_effect? table|{fixed?: string, random?: boolean, ignore?: boolean} Determines how cards with this rank behave when Strength is used. +---@field virtual? table|{ranks: table, next?: table, prev?: table} If set, replaces a rank or a rank's .next/.prev with VirtualRanks (used for straight calculation, e.g. Ace turns to Ace_low and Ace_high) +---@field parity? number The parity of the rank, 0 for Even, 1 for odd. +---@field is_royal? boolean Whether this rank allows for a Royal Flush. (Only relevant for displayed hand text by default) ---@field suit_map? table For any suit keys in this table, use this rank's atlas over the suit's atlas. Provided number is the `y` position of the suit on the rank's atlas. ---@field __call? fun(self: SMODS.Rank|table, o: SMODS.Rank|table): nil|table|SMODS.Rank ---@field extend? fun(self: SMODS.Rank|table, o: SMODS.Rank|table): table Primary method of creating a class. @@ -37,6 +39,7 @@ ---@field draw? fun(self: SMODS.Rank|table, card: Card|table, layer: string) Allows drawing additional sprites or shaders onto cards with this suit. ---@field in_pool? fun(self: SMODS.Rank|table, args: table): boolean? Allows configuring if cards with this suit should spawn. ---@field delete? fun(self: SMODS.Rank|table) Deletes this suit. +---@field get_straight_next? fun(self: SMODS.Rank|table, direction: "next"|"prev", do_wrap?: boolean) Function to get a rank's .next/.prev with respect to VirtualRanks. ---@overload fun(self: SMODS.Rank): SMODS.Rank SMODS.Rank = setmetatable({}, { __call = function(self) diff --git a/lsp_def/classes/sticker.lua b/lsp_def/classes/sticker.lua index f7d1f6d34..89bf27c94 100644 --- a/lsp_def/classes/sticker.lua +++ b/lsp_def/classes/sticker.lua @@ -55,9 +55,3 @@ function Card:add_sticker(sticker, bypass_check) end ---@param sticker Stickers|string Key to the sticker to remove. --- Removes the sticker from the card, if it has the sticker. function Card:remove_sticker(sticker) end - ----@param self Card|table ----@param key string ----@return table? ---- Calculates Stickers on cards. -function Card:calculate_sticker(context, key) end diff --git a/lsp_def/classes/virtual_rank.lua b/lsp_def/classes/virtual_rank.lua new file mode 100644 index 000000000..f3719dc9e --- /dev/null +++ b/lsp_def/classes/virtual_rank.lua @@ -0,0 +1,38 @@ +---@meta + +---@class SMODS.VirtualRank: SMODS.GameObject +---@field obj_buffer? string[] Array of keys to all objects registered to this class. +---@field obj_table? table Table of objects registered to this class. +---@field super? SMODS.GameObject|table Parent class. +---@field base_ranks? table A map of rank keys this VirtualRank overrides. +---@field next? Ranks|string[] List of keys to other ranks that come after this VirtualRank. +---@field prev? Ranks|string[] List of keys to other ranks that come before this VirtualRank. +---@field next_redirs? table A map of ranks this VirtualRank overrides .next for. If value is false, doesn't add self to redirs but still removes base .next +---@field prev_redirs? table A map of ranks this VirtualRank overrides .prev for. If value is false, doesn't add self to redirs but still removes base .prev +---@field next_wrap? table A map of ranks this VirtualRank additionally returns in get_straight_next("next") if do_wrap = true. +---@field prev_wrap? table A map of ranks this VirtualRank additionally returns in get_straight_next("prev") if do_wrap = true. +---@field __call? fun(self: SMODS.Rank|table, o: SMODS.Rank|table): nil|table|SMODS.Rank +---@field extend? fun(self: SMODS.Rank|table, o: SMODS.Rank|table): table Primary method of creating a class. +---@field check_duplicate_register? fun(self: SMODS.Rank|table): boolean? Ensures objects already registered will not register. +---@field check_duplicate_key? fun(self: SMODS.Rank|table): boolean? Ensures objects with duplicate keys will not register. Checked on `__call` but not `take_ownership`. For take_ownership, the key must exist. +---@field register? fun(self: SMODS.Rank|table) Registers the object. +---@field check_dependencies? fun(self: SMODS.Rank|table): boolean? Returns `true` if there's no failed dependencies. +---@field process_loc_text? fun(self: SMODS.Rank|table) Called during `inject_class`. Handles injecting loc_text. +---@field send_to_subclasses? fun(self: SMODS.Rank|table, func: string, ...: any) Starting from this class, recusively searches for functions with the given key on all subordinate classes and run all found functions with the given arguments. +---@field pre_inject_class? fun(self: SMODS.Rank|table) Called before `inject_class`. Injects and manages class information before object injection. +---@field post_inject_class? fun(self: SMODS.Rank|table) Called after `inject_class`. Injects and manages class information after object injection. +---@field inject_class? fun(self: SMODS.Rank|table) Injects all direct instances of class objects by calling `obj:inject` and `obj:process_loc_text`. Also injects anything necessary for the class itself. Only called if class has defined both `obj_table` and `obj_buffer`. +---@field inject? fun(self: SMODS.Rank|table, i?: number) Called during `inject_class`. Injects the object into the game. +---@field take_ownership? fun(self: SMODS.Rank|table, key: string, obj: SMODS.Rank|table, silent?: boolean): nil|table|SMODS.Rank Takes control of vanilla objects. Child class must have get_obj for this to function +---@field get_obj? fun(self: SMODS.Rank|table, key: string): SMODS.Rank|table? Returns an object if one matches the `key`. +---@field delete? fun(self: SMODS.Rank|table) Deletes this suit. +---@field get_straight_next? fun(self: SMODS.VirtualRank|table, direction: "next"|"prev", do_wrap?: boolean) Function to get a virtual rank's .next/.prev, added to be interchangeable with SMODS.Rank. +---@overload fun(self: SMODS.Rank): SMODS.Rank +SMODS.VirtualRank = setmetatable({}, { + __call = function(self) + return self + end +}) + +---@type table +SMODS.VirtualRanks = {} diff --git a/lsp_def/utils.lua b/lsp_def/utils.lua index 238d543a1..b5568c15e 100644 --- a/lsp_def/utils.lua +++ b/lsp_def/utils.lua @@ -104,6 +104,14 @@ ---@field new_suit? Suits|string New suit the card changed to. ---@field old_suit? Suits|string Old suit the card changed from. ---@field round_eval? true Check if `true` for effects during round evaluation (cashout screen). +---@field _quantum_getter? true Internal flag for the general quantum getter context, see per-field flags below. +---@field get_ranks? true Check if `true` for modifying the rank(s) of the context.card. `context.ranks` is a map of `SMODS.Rank` keys. Return `ranks` to add or remove ranks, and return `fixed = 'ranks'` (or `fixed = {ranks = true}`) to override the `context.ranks` entirely. +---@field get_enhancements? true Check if `true` for modifying the enhancement(s) of the context.card. ^ +---@field get_seals? true Check if `true` for modifying the seal(s) of the context.card. ^ +---@field get_editions? true Check if `true` for modifying the edition(s) of the context.card. ^ +---@field get_stickers? true Check if `true` for modifying the sticker(s) of the context.card. ^ +---@field get_suits? true Check if `true` for modifying the suit(s) of the context.card. ^ +---@field no_mod? boolean Check if `true` to decide whether an effect should modify context.get_ranks' ranks field. (If you want to override and block other effects, return `no_mod=true` alongside ranks=[only your ranks] and fixed=true) ---@field money_altered? true Check if `true` for effects when the amount of money the player has changes. ---@field from_shop? true Check if `true` if money changed during the shop. ---@field from_consumeable? true Check if `true` if money changed by a consumable. @@ -139,7 +147,7 @@ function SMODS.merge_lists(...) end --- A table of SMODS feature that mods can choose to enable. ---@class SMODS.optional_features: table ----@field quantum_enhancements? boolean Enables "Quantum Enhancement" contexts. Cards can count as having multiple enhancements at once. +---@field quantum_fields? table Map of SMODS.QuantumCardField keys and whether the corresponding field is toggled on. Allows a playing card to have multiple values for Ranks, Enhancements, Seals, Editions or Stickers. ---@field retrigger_joker? boolean Enables "Joker Retrigger" contexts. Jokers can be retriggered by other jokers or effects. ---@field post_trigger? boolean Enables "Post Trigger" contexts. Allows calculating effects after a Joker has been calculated. ---@field object_weights? boolean Enables individual weights for object polling. @@ -150,7 +158,7 @@ function SMODS.merge_lists(...) end ---@field discard? boolean Enables "Discard Calculation". Discarded cards are included in calculation. ---@type SMODS.optional_features -SMODS.optional_features = { cardareas = {} } +SMODS.optional_features = { cardareas = {}, quantum_fields = {} } --- Inserts all SMODS features enabled by loaded mods into `SMODS.optional_features`. function SMODS.get_optional_features() end @@ -270,24 +278,6 @@ function SMODS.smart_level_up_hand(card, hand, instant, amount) end --- Returns table of CardAreas. function SMODS.get_card_areas(_type, _context) end ----@param card Card|table ----@param extra_only? boolean Return table will not have the card's actual enhancement. ----@return table enhancements ---- Returns table of enhancements the provided `card` has. -function SMODS.get_enhancements(card, extra_only) end - ----@param card Card|table ----@param key Enhancements|string ----@return boolean ---- Checks if this card a specific enhancement. -function SMODS.has_enhancement(card, key) end - ----@param card Card|table ----@param effects table ----@param context CalcContext|table ---- Calculates quantum Enhancements. Require `SMODS.optional_features.quantum_enhancements` to be `true`. -function SMODS.calculate_quantum_enhancements(card, effects, context) end - ---@param card Card|table ---@return boolean? --- Check if the card should shatter. @@ -303,10 +293,6 @@ function SMODS.has_no_suit(card) end --- Checks if the card counts as having all suits. function SMODS.has_any_suit(card) end ----@param card Card|table ----@return boolean? ---- Checks if the card counts as having no rank. -function SMODS.has_no_rank(card) end ---@param card Card|table ---@return boolean? @@ -755,6 +741,15 @@ function SMODS.is_poker_hand_visible(handname) end --- `trigger` is the card or effect that runs the check function SMODS.is_eternal(card, trigger) end +---@param id number +---@return SMODS.Rank rank +--- Returns the first rank from SMODS.Ranks whose .id == [id]. +function SMODS.get_rank_from_id(id) end + +---@param cards table +---@return table<"lowest"|"highest", table<"rank"|"cards", SMODS.Rank|table>> +function SMODS.lowest_and_highest_rank(cards) end + ---@param card Card|table ---@param args? table|{ref_table: table, ref_value: string, scalar_value: string?, scalar_table: table?, scalar_factor:number?, operation: '+'|'X'|'-'|string|fun(ref_table: table, ref_value: string, initial: number, change: number)?, block_overrides: {value: boolean?, scalar: boolean?, message: boolean?}?, scaling_message: table?, message_key: string?, message_colour: table?, message_delay: number?, no_message: boolean?} ---@return number, number @@ -813,11 +808,11 @@ function SMODS.get_previous_context() end function SMODS.update_context_flags(context, flags) end ---@param context CalcContext|table The context checked ----@return string|false ---- Either returns a getter context identifier string +---@return string|nil +--- Either returns a context identifier string (from SMODS.CONTEXT_TYPES enum) --- (e.g. "enhancement" for context.check_enhancement) ---- or false if the [context] isn't a getter context. -function SMODS.is_getter_context(context) end +--- or nil if the [context] isn't a defined context. +function SMODS.get_context_type(context) end ---@param context CalcContext|table The context checked ---@return boolean @@ -837,7 +832,29 @@ function SMODS.can_context_post_trigger(context) end --- skipping the evaluation of the object and preventing an infinite loop. function SMODS.check_looping_context(eval_object) end ----@param atlas_key string The key of the atlas +-- Todo : Complete QuantumCardField lsp defs + +---@param parity integer The parity to be checked. (See SMODS.Rank lsp def) +---@return boolean +function Card:is_parity(parity) end + +---@param t? table The list to turn into a map +---@return table +--- This function turns a list into a mapping, so for {k: v} -> {v: true} +function SMODS.to_map(t) end + +---@param t? table The table from which to get the keys. +---@return table +--- This function gets the keys of a table and returns them as a list +function SMODS.get_keys(t) end + +---@param t? table The table of ranks to use as reference, SMODS.Ranks by default +---@param objectified? boolean Whether to return a map of 'SMODS.Rank's or rank keys +---@return table +--- Helper function for straight calculation, gets SMODS.Ranks with respect to VirtualRanks +function SMODS.get_straight_ranks(t, objectified) end + +---@param atlas_key string The key of the atlas --- This function gets an atlas from G.ASSET_ATLAS or G.ANIMATION_ATLAS function SMODS.get_atlas(atlas_key) end diff --git a/src/card_draw.lua b/src/card_draw.lua index 18744ea99..cee099673 100644 --- a/src/card_draw.lua +++ b/src/card_draw.lua @@ -330,14 +330,14 @@ SMODS.DrawStep { key = 'seal', order = 30, func = function(self, layer) - local seal = G.P_SEALS[self.seal] or {} - if self.ability.delay_seal then return end + local key = self.delay_seal or self.seal + local seal = G.P_SEALS[key] or {} if type(seal.draw) == 'function' then seal:draw(self, layer) - elseif self.seal then - G.shared_seals[self.seal].role.draw_major = self - G.shared_seals[self.seal]:draw_shader('dissolve', nil, nil, nil, self.children.center) - if self.seal == 'Gold' then G.shared_seals[self.seal]:draw_shader('voucher', nil, self.ARGS.send_to_shader, nil, self.children.center) end + elseif type(key) == "string" then + G.shared_seals[key].role.draw_major = self + G.shared_seals[key]:draw_shader('dissolve', nil, nil, nil, self.children.center) + if key == 'Gold' then G.shared_seals[key]:draw_shader('voucher', nil, self.ARGS.send_to_shader, nil, self.children.center) end end end, conditions = { vortex = false, facing = 'front' }, diff --git a/src/game_object.lua b/src/game_object.lua index 265ea2b74..419d0497a 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -1981,7 +1981,6 @@ SMODS.UndiscoveredCompat = { ------------------------------------------------------------------------------------------------- ----- API CODE GameObject.Suit ------------------------------------------------------------------------------------------------- - SMODS.inject_p_card = function(suit, rank) G.P_CARDS[suit.card_key .. '_' .. rank.card_key] = { name = rank.key .. ' of ' .. suit.key, @@ -2157,7 +2156,13 @@ SMODS.UndiscoveredCompat = { }, next = {}, prev = nil, - straight_edge = false, + virtual = { + ranks = nil, + next = nil, + prev = nil, + }, + parity = nil, + is_royal = nil, -- TODO we need a better system for what this is doing. -- We should allow setting a playing card's atlas and position to any values, -- and we should also ensure that it's easy to create an atlas with a standard @@ -2216,15 +2221,20 @@ SMODS.UndiscoveredCompat = { SMODS.process_loc_text(G.localization.misc.ranks, self.key, self.loc_txt, 'name') end, inject = function(self) - for _,v in ipairs(self.next) do - local other = self.obj_table[v] + for k, _ in pairs(self.next) do + local other = self.obj_table[k] if other then - table.insert(other.prev, self.key) + other.prev[self.key] = true end end for _, suit in pairs(SMODS.Suits) do SMODS.inject_p_card(suit, self) end + self.virtual = { -- Redefine so it's not shared between ranks + ranks = {}, + next = nil, + prev = nil, + } end, delete = function(self) local i @@ -2236,16 +2246,37 @@ SMODS.UndiscoveredCompat = { end self.used_card_keys[self.card_key] = nil table.remove(self.obj_buffer, i) + end, + get_straight_next = function (self, direction, do_wrap) + local dir = direction == "prev" and "prev" or "next" + return self.virtual[dir] or self[dir] end } - for _, v in ipairs({ 2, 3, 4, 5, 6, 7, 8, 9 }) do + SMODS.Rank { + key = '2', + card_key = '2', + pos = { x = 0 }, + nominal = 2, + next = { ['3'] = true }, + parity = 0, + strength_effect = { + fixed_next = '3', + fixed_prev = 'Ace' + }, + } + for _, v in ipairs({ 3, 4, 5, 6, 7, 8, 9 }) do SMODS.Rank { key = v .. '', card_key = v .. '', pos = { x = v - 2 }, nominal = v, sort_id = v - 1, - next = { (v + 1) .. '' }, + next = { [(v + 1) .. ''] = true }, + parity = math.fmod(v, 2), + strength_effect = { + fixed_next = (v + 1) .. '', + fixed_prev = (v - 1) .. '', + }, } end SMODS.Rank { @@ -2254,7 +2285,13 @@ SMODS.UndiscoveredCompat = { pos = { x = 8 }, nominal = 10, sort_id = 9, - next = { 'Jack' }, + next = { Jack = true }, + parity = 0, + is_royal = true, + strength_effect = { + fixed_next = 'Jack', + fixed_prev = '9' + }, } SMODS.Rank { key = 'Jack', @@ -2265,7 +2302,13 @@ SMODS.UndiscoveredCompat = { face = true, sort_id = 10, shorthand = 'J', - next = { 'Queen' }, + next = { Queen = true }, + parity = nil, + is_royal = true, + strength_effect = { + fixed_next = 'Queen', + fixed_prev = '10' + }, } SMODS.Rank { key = 'Queen', @@ -2276,7 +2319,13 @@ SMODS.UndiscoveredCompat = { face = true, sort_id = 11, shorthand = 'Q', - next = { 'King' }, + next = { King = true }, + parity = nil, + is_royal = true, + strength_effect = { + fixed_next = 'King', + fixed_prev = 'Jack' + }, } SMODS.Rank { key = 'King', @@ -2287,7 +2336,13 @@ SMODS.UndiscoveredCompat = { face = true, sort_id = 12, shorthand = 'K', - next = { 'Ace' }, + next = { Ace = true }, + parity = nil, + is_royal = true, + strength_effect = { + fixed_next = 'Ace', + fixed_prev = 'Queen' + }, } SMODS.Rank { key = 'Ace', @@ -2297,9 +2352,24 @@ SMODS.UndiscoveredCompat = { face_nominal = 0.4, sort_id = 13, shorthand = 'A', - straight_edge = true, - next = { '2' }, + next = { ['2'] = true }, + parity = 1, + is_royal = true, + strength_effect = { + fixed_next = '2', + fixed_prev = 'King' + }, } + + ------------------------------------------------------------------------------------------------- + ----- API IMPORT GameObject.VirtualRank + ------------------------------------------------------------------------------------------------- + + assert(load(NFS.read(SMODS.path..'src/game_objects/virtual_ranks.lua'), ('=[SMODS _ "src/game_objects/virtual_ranks.lua"]')))() + + ------------------------------------------------------------------------------------------------- + + -- make consumable effects compatible with added suits -- TODO put this in utils.lua local function juice_flip(used_tarot) @@ -2957,8 +3027,17 @@ SMODS.UndiscoveredCompat = { return { SMODS.merge_lists(parts._5, parts._flush) } end, ['Flush House'] = function(parts) - if #parts._3 < 1 or #parts._2 < 2 or not next(parts._flush) then return {} end - return { SMODS.merge_lists(parts._all_pairs, parts._flush) } + if not SMODS.optional_features.quantum_fields.rank then + if #parts._3 > 0 and #parts._2 > 1 and next(parts._flush) then + return { SMODS.merge_lists(parts._all_pairs, parts._flush) } + end + else + local full_house = get_quantum_pairing({{pairs = parts._2, min_len = 2}, {pairs = parts._3, min_len = 3}}) or {} + if next(full_house) and next(parts._flush) then + return { SMODS.merge_lists(full_house, parts._flush) } + end + end + return {} end, ['Five of a Kind'] = function(parts) return parts._5 end, ['Straight Flush'] = function(parts) @@ -2967,15 +3046,27 @@ SMODS.UndiscoveredCompat = { end, ['Four of a Kind'] = function(parts) return parts._4 end, ['Full House'] = function(parts) - if #parts._3 < 1 or #parts._2 < 2 then return {} end - return parts._all_pairs + if not SMODS.optional_features.quantum_fields.rank then + if #parts._3 > 0 and #parts._2 > 1 then + return parts._all_pairs + end + else + return get_quantum_pairing({{pairs = parts._2, min_len = 2}, {pairs = parts._3, min_len = 3}}) or {} + end + return {} end, ['Flush'] = function(parts) return parts._flush end, ['Straight'] = function(parts) return parts._straight end, ['Three of a Kind'] = function(parts) return parts._3 end, ['Two Pair'] = function(parts) - if #parts._2 < 2 then return {} end - return parts._all_pairs + if not SMODS.optional_features.quantum_fields.rank then + if #parts._2 > 1 then + return parts._all_pairs + end + else + return get_quantum_pairing({{pairs = parts._2, min_len = 2}, {pairs = parts._2, min_len = 2}}) or {} + end + return {} end, ['Pair'] = function(parts) return parts._2 end, ['High Card'] = function(parts) return parts._highest end, @@ -3309,6 +3400,10 @@ SMODS.UndiscoveredCompat = { ----- API CODE GameObject.Enhancement ------------------------------------------------------------------------------------------------- + SMODS.Enhancements = {} + for _, k in ipairs({"c_base", "m_bonus", "m_mult", "m_wild", "m_glass", "m_steel", "m_stone", "m_gold", "m_lucky"}) do + SMODS.Enhancements[k] = G.P_CENTERS[k] + end SMODS.Enhancement = SMODS.Center:extend { set = 'Enhanced', class_prefix = 'm', @@ -3339,6 +3434,7 @@ SMODS.UndiscoveredCompat = { self.config = self.config or {} assert(not (self.no_suit and self.any_suit), "Cannot have both \"no_suit\" and \"any_suit\" defined in a SMODS.Enhancement object.") if self.no_rank then self.overrides_base_rank = true end + SMODS.Enhancements[self.key] = self SMODS.Enhancement.super.register(self) end, -- Produces the description of the whole playing card @@ -3395,6 +3491,7 @@ SMODS.UndiscoveredCompat = { -- }) SMODS.Enhancement:take_ownership('glass', { + shatters = true, calculate = function(self, card, context) if context.destroy_card and context.cardarea == G.play and context.destroy_card == card and SMODS.pseudorandom_probability(card, 'glass', 1, card.ability.extra) then card.glass_trigger = true @@ -3508,6 +3605,10 @@ SMODS.UndiscoveredCompat = { ----- API CODE GameObject.Edition ------------------------------------------------------------------------------------------------- + SMODS.Editions = {} + for _, k in ipairs({"e_base", "e_foil", "e_holo", "e_polychrome", "e_negative"}) do + SMODS.Editions[k] = G.P_CENTERS[k] + end SMODS.Edition = SMODS.Center:extend { set = 'Edition', -- atlas only matters for displaying editions in the collection @@ -3545,6 +3646,7 @@ SMODS.UndiscoveredCompat = { generic = string.sub(self.key, 3) .. '_generic' .. '_SMODS_INTERNAL' } end + SMODS.Editions[self.key] = self SMODS.Edition.super.register(self) end, process_loc_text = function(self) @@ -3595,18 +3697,7 @@ SMODS.UndiscoveredCompat = { SMODS.Edition:take_ownership('foil', { shader = 'foil', - config = setmetatable({ chips = 50 }, { - __index = function(t, k) - if k == 'extra' then return t.chips end - return rawget(t, k) - end, - __newindex = function(t, k, v) - if k == 'extra' then - t.chips = v; return - end - rawset(t, k, v) - end, - }), + config = { extra = { chips = 50 } }, sound = { sound = "foil1", per = 1.2, vol = 0.4 }, weight = 20, extra_cost = 2, @@ -3616,30 +3707,19 @@ SMODS.UndiscoveredCompat = { return G.GAME.edition_rate * self.weight end, loc_vars = function(self, info_queue, card) - return { vars = { card.edition.chips } } + return { vars = { card.edition.extra.chips } } end, calculate = function(self, card, context) if context.pre_joker or (context.main_scoring and context.cardarea == G.play) then return { - chips = card.edition.chips + chips = ((card.ability.edition or {}).extra or {}).chips } end end }) SMODS.Edition:take_ownership('holo', { shader = 'holo', - config = setmetatable({ mult = 10 }, { - __index = function(t, k) - if k == 'extra' then return t.mult end - return rawget(t, k) - end, - __newindex = function(t, k, v) - if k == 'extra' then - t.mult = v; return - end - rawset(t, k, v) - end, - }), + config = { extra = { mult = 10 } }, sound = { sound = "holo1", per = 1.2 * 1.58, vol = 0.4 }, weight = 14, extra_cost = 3, @@ -3649,30 +3729,19 @@ SMODS.UndiscoveredCompat = { return G.GAME.edition_rate * self.weight end, loc_vars = function(self, info_queue, card) - return { vars = { card.edition.mult } } + return { vars = { card.edition.extra.mult } } end, calculate = function(self, card, context) if context.pre_joker or (context.main_scoring and context.cardarea == G.play) then return { - mult = card.edition.mult + mult = ((card.ability.edition or {}).extra or {}).mult } end end }) SMODS.Edition:take_ownership('polychrome', { shader = 'polychrome', - config = setmetatable({ x_mult = 1.5 }, { - __index = function(t, k) - if k == 'extra' then return t.x_mult end - return rawget(t, k) - end, - __newindex = function(t, k, v) - if k == 'extra' then - t.x_mult = v; return - end - rawset(t, k, v) - end, - }), + config = { extra = { x_mult = 1.5 } }, sound = { sound = "polychrome1", per = 1.2, vol = 0.7 }, weight = 3, extra_cost = 5, @@ -3682,12 +3751,12 @@ SMODS.UndiscoveredCompat = { return (G.GAME.edition_rate - 1) * G.P_CENTERS["e_negative"].weight + G.GAME.edition_rate * self.weight end, loc_vars = function(self, info_queue, card) - return { vars = { card.edition.x_mult } } + return { vars = { card.edition.extra.x_mult } } end, calculate = function(self, card, context) if context.post_joker or (context.main_scoring and context.cardarea == G.play) then return { - x_mult = card.edition.x_mult + x_mult = ((card.ability.edition or {}).extra or {}).x_mult } end end @@ -4066,6 +4135,18 @@ SMODS.UndiscoveredCompat = { text = '^' } + ------------------------------------------------------------------------------------------------- + ----- API IMPORT GameObject.CardAbilityField + ------------------------------------------------------------------------------------------------- + + assert(load(NFS.read(SMODS.path..'src/game_objects/card_ability_fields.lua'), ('=[SMODS _ "src/game_objects/card_ability_fields.lua"]')))() + + ------------------------------------------------------------------------------------------------- + ----- API IMPORT GameObject.QuantumCardField + ------------------------------------------------------------------------------------------------- + + assert(load(NFS.read(SMODS.path..'src/game_objects/quantum_card_fields.lua'), ('=[SMODS _ "src/game_objects/quantum_card_fields.lua"]')))() + ------------------------------------------------------------------------------------------------- ----- API IMPORT Object.Node.Moveable.Sprite.AnimatedSprite.StateSprite diff --git a/src/game_objects/card_ability_fields.lua b/src/game_objects/card_ability_fields.lua new file mode 100644 index 000000000..4ca0e57e8 --- /dev/null +++ b/src/game_objects/card_ability_fields.lua @@ -0,0 +1,341 @@ +SMODS.CARD_VALUE_TYPES = { + ADDITIVE = "additive", + MULTIPLICATIVE = "multiplicative" +} + +SMODS.CARD_VALUE_REFS = {} -- Unused, map for all the CardAbilityFields' variant refs. + + +-- Helper function to get a card's qfield-cached abilities, or if uncached just card.ability +function SMODS.get_card_abilities(card) + if (SMODS.qfield_cache[card] or {}).abilities then + return SMODS.qfield_cache[card].abilities + end + local fallback = card._base_ability and {{t = card._base_ability}} or card.ability and {{t = card.ability}} or {} + if not SMODS.set_quantum_cache(card) then return fallback end + return (SMODS.qfield_cache[card] or {}).abilities or fallback +end + +SMODS.CardAbilityFields = {} +SMODS.CardAbilityField = SMODS.GameObject:extend { + obj_table = SMODS.CardAbilityFields, + set = 'CardAbilityField', + obj_buffer = {}, + required_params = { + 'key', + }, + process_loc_text = function() end, + inject = function(self) + local inject_args = self.inject_args or {} + if not inject_args.no_getter then + local _getter = function (card, ...) + local abilities = SMODS.get_card_abilities(card) + return self:getter(abilities, card, ...) + end + Card["get_" .. self.key] = _getter + end + self.value_ref = self.value_ref or self.calc_key + if self.value_ref then + self.perma_value_ref = self.perma_value_ref or "perma_" .. self.value_ref + end + if not self.variant_refs then -- This is used mostly instead of the singular value_ref + self.variant_refs = {self.value_ref} + end + for _, v_ref in ipairs(self.variant_refs) do + SMODS.CARD_VALUE_REFS[v_ref] = true + end + if not self.default_value then + if self.stacking_type == SMODS.CARD_VALUE_TYPES.ADDITIVE then self.default_value = 0 + elseif self.stacking_type == SMODS.CARD_VALUE_TYPES.MULTIPLICATIVE then self.default_value = 1 end + end + self.start_value = self.start_value or self.default_value + self.value_offset = self.value_offset or 0 + end, + scoring_card_areas = { + play = true, + }, + post_inject_class = function(self) end, + calc_key = nil, -- e.g. "mult" + value_ref = nil, -- property path relative to Card.ability, e.g. "x_chips" + variant_refs = nil, -- list of the above, used for compat with e.g. Xmult / x_mult + perma_value_ref = nil, -- ^ for permanent bonus + stacking_type = SMODS.CARD_VALUE_TYPES.ADDITIVE, + max_value = math.huge, + min_value = -math.huge, + start_value = nil, + default_value = nil, + value_offset = nil, + getter = function (self, abilities, card, ...) -- abilities is of form {integer: {t = [ability table], key = [source obj key], qfield_key = [qfield key]}} + -- if self.debuff then return self.default_value end + local ret = self.start_value + for _, ability_t in ipairs(abilities) do + local ability = ability_t.t + if self.stacking_type == SMODS.CARD_VALUE_TYPES.ADDITIVE then + for _, v_ref in ipairs(self.variant_refs) do + local v = (table_get_subfield(ability or {}, v_ref) or self.default_value) + ret = ret + v + self.value_offset + if v ~= (self.default_value + self.value_offset) then break end -- Only apply value once + end + elseif self.stacking_type == SMODS.CARD_VALUE_TYPES.MULTIPLICATIVE then + for _, v_ref in ipairs(self.variant_refs) do + local v = (table_get_subfield(ability or {}, v_ref) or self.default_value) + ret = ret * (v + self.value_offset) + if v ~= (self.default_value + self.value_offset) then break end -- Only apply value once + end + end + end + if self.stacking_type == SMODS.CARD_VALUE_TYPES.MULTIPLICATIVE then + local base = (table_get_subfield(card.ability or {}, self.perma_value_ref) or 0) + ret = ret * (base + 1) + else + ret = ret + (table_get_subfield(card.ability or {}, self.perma_value_ref) or 0) + end + return ret - self.value_offset + end, + ret_table_key = "playing_card", + insert_value = function (self, card, ret_table, ...) + local abilities = SMODS.get_card_abilities(card) + local value = self:getter(abilities, card, ...) + if self.max_value >= value and value >= self.min_value and value ~= self.default_value then + ret_table[self.ret_table_key] = ret_table[self.ret_table_key] or {} + ret_table[self.ret_table_key][self.calc_key] = value + end + end, + context_criteria = nil, + check_context_criteria = function (self, context) + local valid = false + for card_area_key, _ in pairs(self.scoring_card_areas) do + if context.cardarea == G[card_area_key] then + valid = true + break + end + end + if not valid then return false end + for c_key, v in pairs(self.context_criteria or {}) do + if context[c_key] ~= v then return false end + end + return true + end + +} + +SMODS.CardAbilityField{ + key = "repetitions", + calc_key = "repetitions", + scoring_card_areas = {}, + insert_value = function (self, card, ret_table, ...) + local abilities = SMODS.get_card_abilities(card) + local reps = self:getter(abilities, card, ...) + if reps > 0 then + ret_table.seals = ret_table.seals or { card = card, message = localize('k_again_ex') } + ret_table.seals.repetitions = (ret_table.seals.repetitions and ret_table.seals.repetitions + reps) or reps + end + end +} + +SMODS.CardAbilityField{ + key = "chip_bonus", + calc_key = "chips", + variant_refs = {"chips", "bonus"}, + getter = function(self, abilities, card, ...) + local base = card.base.nominal or 0 + local ret = 0 + for _, ability_t in ipairs(abilities) do + local ability = ability_t.t or {} + if ability.effect == "Stone Card" or ((ability_t.qfield_key and SMODS.QuantumCardFields[ability_t.qfield_key].g_obj_table[ability_t.key] or {}).replace_base_card) then + base = 0 + end + ret = ret + (ability.chips or 0) + (ability.bonus or 0) + end + return base + ret + (card.ability.perma_chips or 0) + end +} + +SMODS.CardAbilityField{ + key = "chip_mult", + calc_key = "mult", + getter = function(self, abilities, card, ...) + local ret = 0 + for _, ability_t in ipairs(abilities) do + local ability = ability_t.t or {} + if ability.effect == "Lucky Card" then + if SMODS.pseudorandom_probability(card, 'lucky_mult', 1, 5) then + card.lucky_trigger = true + ret = ret + ability.mult + end + else + ret = ret + (ability.mult or 0) + end + end + return ret + (card.ability.perma_mult or 0) + end +} + +SMODS.CardAbilityField{ + key = "chip_x_mult", + stacking_type = SMODS.CARD_VALUE_TYPES.MULTIPLICATIVE, + calc_key = "x_mult", + variant_refs = {"x_mult", "Xmult"}, +} + +SMODS.CardAbilityField{ + key = "chip_h_mult", + calc_key = "h_mult", + scoring_card_areas = {hand = true}, +} + +SMODS.CardAbilityField{ + key = "chip_h_x_mult", + stacking_type = SMODS.CARD_VALUE_TYPES.MULTIPLICATIVE, + default_value = 0, + start_value = 1, + value_offset = 1, + value_ref = "h_x_mult", + calc_key = "x_mult", + scoring_card_areas = {hand = true}, +} + +SMODS.CardAbilityField{ + key = "chip_x_bonus", + stacking_type = SMODS.CARD_VALUE_TYPES.MULTIPLICATIVE, + calc_key = "x_chips", +} + +SMODS.CardAbilityField{ + key = "chip_h_bonus", + calc_key = "h_chips", + scoring_card_areas = {hand = true}, +} + +SMODS.CardAbilityField{ + key = "chip_h_x_bonus", + stacking_type = SMODS.CARD_VALUE_TYPES.MULTIPLICATIVE, + value_ref = "h_x_chips", + calc_key = "x_chips", + scoring_card_areas = {hand = true}, +} + +SMODS.CardAbilityField{ + key = "h_dollars", + calc_key = "h_dollars", + scoring_card_areas = {hand = true}, + context_criteria = {playing_card_end_of_round = true}, +} + +SMODS.CardAbilityField{ + key = "p_dollars", + calc_key = "p_dollars", + getter = function (self, abilities, card, ...) + local ret = 0 + for key, q_field in pairs(SMODS.QuantumCardFields) do + local values = q_field.getter(card) + for k, v in pairs(values) do + local obj = q_field.g_obj_table[k] or {} + if obj.get_p_dollars and type(obj.get_p_dollars) == 'function' then + ret = ret + obj:get_p_dollars(card) + end + end + end + if card:has_seal("Gold") then + ret = ret + 3 + end + for _, ability_t in ipairs(abilities) do + local ability = ability_t.t or {} + if ability.effect == "Lucky Card" then + if SMODS.pseudorandom_probability(card, 'lucky_money', 1, 15) then + card.lucky_trigger = true + ret = ret + (ability.p_dollars or 0) + end + else + ret = ret + (ability.p_dollars or 0) + end + end + if ret ~= 0 then + G.GAME.dollar_buffer = (G.GAME.dollar_buffer or 0) + ret + G.E_MANAGER:add_event(Event({func = (function() G.GAME.dollar_buffer = 0; return true end)})) + end + return ret + (card.ability.perma_p_dollars or 0) + end +} + +SMODS.CardAbilityField{ + key = "bonus_score", + calc_key = "score", +} + +SMODS.CardAbilityField{ + key = "bonus_x_score", + calc_key = "x_score", + stacking_type = SMODS.CARD_VALUE_TYPES.MULTIPLICATIVE +} + +SMODS.CardAbilityField{ + key = "bonus_h_score", + calc_key = "h_score", + scoring_card_areas = {hand = true}, +} + +SMODS.CardAbilityField{ + key = "bonus_h_x_score", + calc_key = "h_x_score", + scoring_card_areas = {hand = true}, + stacking_type = SMODS.CARD_VALUE_TYPES.MULTIPLICATIVE +} + +SMODS.CardAbilityField{ + key = "bonus_blind_size", + calc_key = "blind_size", +} + +SMODS.CardAbilityField{ + key = "bonus_x_blind_size", + calc_key = "x_blind_size", + stacking_type = SMODS.CARD_VALUE_TYPES.MULTIPLICATIVE +} + +SMODS.CardAbilityField{ + key = "bonus_h_blind_size", + value_ref = "h_blind_size", + calc_key = "blind_size", + scoring_card_areas = {hand = true}, +} + +SMODS.CardAbilityField{ + key = "bonus_h_x_blind_size", + value_ref = "h_x_blind_size", + calc_key = "x_blind_size", + scoring_card_areas = {hand = true}, + stacking_type = SMODS.CARD_VALUE_TYPES.MULTIPLICATIVE +} + + +-- Helper function to get a sanitized ability table +function SMODS.get_ability_from_obj(obj, card) + local ability, skip + if type(obj.cache_ability) == "function" then + ability, skip = obj:cache_ability(card) + end + if skip then return ability end + local config = obj.config + if config then + ability = ability or {} + for key, ca_field in pairs(SMODS.CardAbilityFields) do + for _, variant in ipairs(ca_field.variant_refs) do + if config[variant] then + ability[ca_field.value_ref] = config[variant] + break + end + end + end + ability.name = obj.name + ability.effect = obj.effect + ability.set = obj.set + ability.h_size = config.h_size or 0 + ability.d_size = config.d_size or 0 + ability.card_limit = config.card_limit + ability.extra = ability.extra or copy_table(config.extra) or nil + ability.extra_value = 0 + ability.type = ability.type or '' + end + return ability +end \ No newline at end of file diff --git a/src/game_objects/quantum_card_fields.lua b/src/game_objects/quantum_card_fields.lua new file mode 100644 index 000000000..85e5c4c16 --- /dev/null +++ b/src/game_objects/quantum_card_fields.lua @@ -0,0 +1,911 @@ +-- General helpers +local _default_enabled = false + +-- Card QField Cache +SMODS.qfield_cache = {} + +local game_update_ref = Game.update +function Game:update(dt) + local ret = game_update_ref(self, dt) + SMODS.qfield_cache = {} + return ret +end + +-- Returns SMODS.qfield_cache[card].get, sanitized to use only string keys +local function _general_quantum_getter(card, args) + args = args or {} + if (SMODS.qfield_cache[card] or {}).get then + if args.as_objs then + local ret = {} + local eval = SMODS.qfield_cache[card].get + for key, q_field in pairs(SMODS.QuantumCardFields) do + ret[q_field.return_flag] = {} + for k, _ in pairs(eval[q_field.return_flag]) do + local obj = q_field.g_obj_table[type(k) == "table" and k.key or k] or {[k] = true, key = k} -- This table supports fake values which don't actually exist as an object. + ret[q_field.return_flag][obj] = true + end + end + return ret + else + return SMODS.qfield_cache[card].get + end + end + SMODS.qfield_cache[card] = { + has = {}, + get = {} + } + -- setup for SMODS.qfield_cache[card].get and SMODS.qfield_cache[card].has + local has_context = {card_has_check = true, card = card, no_mod = false} + local flags = args.has_flags or args + for key, q_field in pairs(SMODS.QuantumCardFields) do + SMODS.qfield_cache[card].get[q_field.return_flag] = q_field:base_getter(card, args) or {} + SMODS.qfield_cache[card].has[key] = {} + has_context[q_field.has_context_flag] = true + end + for key, flag in pairs(flags) do + has_context[key] = flag + end + if next(SMODS.optional_features.quantum_fields) and not args._no_contexts then -- Only calculate the context if any quantum fields are toggled on + SMODS.calculate_context(has_context) -- SMODS.qfield_cache[card].has is updated directly in SMODS.update_context_flags() + end + for key, q_field in pairs(SMODS.QuantumCardFields) do + for k, v in pairs(SMODS.qfield_cache[card].get[q_field.return_flag]) do -- For every value + local obj = q_field.g_obj_table[k] or {} + for other_key, _ in pairs(SMODS.QuantumCardFields) do -- Check every has flag + if obj["no_" .. other_key] then SMODS.qfield_cache[card].has[other_key].no = true end + if obj["any_" .. other_key] then SMODS.qfield_cache[card].has[other_key].any = true end + end + end + end + + -- _quantum_getter context for SMODS.qfield_cache[card].get + local get_context = {_quantum_getter = true, card = card, no_mod = false} -- _quantum_getter flag should not be referenced in practice (as it doesn't account for optional_features.quantum_fields), use specific "get_ranks" etc. flags instead + local has = SMODS.qfield_cache[card].has + local do_calculate_getter = false + for key, q_field in pairs(SMODS.QuantumCardFields) do + local get = SMODS.qfield_cache[card].get[q_field.return_flag] + local override_get = (has[key].no and not has[key].any and {}) or (has[key].any and SMODS.shallow_copy(q_field.g_obj_table)) -- If e.g. has.rank.no is true and .any not, default to no ranks, if any is true, default to all ranks, if neither, default to the values set by the above has_context + if override_get then + local new_get = {} + for k, v in pairs(override_get) do + new_get[k] = (get[k] ~= "BASE" or not v) and not not v or "BASE" -- If it's a "BASE" value and it's not removed, keep it as "BASE" -> Required for correct quantum card.ability access + end + get = new_get + SMODS.qfield_cache[card].get[q_field.return_flag] = new_get + end + if SMODS.optional_features.quantum_fields[key] and not has[key].any then -- Todo : Discuss whether `any = true` should be stronger or weaker than (getter) quantum effects. Currently this check makes it stronger. + get_context[q_field.get_context_flag] = true + get_context[q_field.return_flag] = get + do_calculate_getter = true + end + end + local flags = args.get_flags or args + for key, flag in pairs(flags) do + get_context[key] = flag + end + if do_calculate_getter and not args._no_contexts then + SMODS.calculate_context(get_context) -- SMODS.qfield_cache[card].get is updated directly in SMODS.update_context_flags() + end + -- Prepare ret + local eval = SMODS.qfield_cache[card].get + local ret = {} + for key, q_field in pairs(SMODS.QuantumCardFields) do + ret[q_field.return_flag] = {} + for k, v in pairs(eval[q_field.return_flag]) do + if v then + local string_key = type(k) == "table" and k.key or k + local obj = q_field.g_obj_table[string_key] + local ret_key = args.as_objs and obj or string_key + ret[q_field.return_flag][ret_key] = true + if obj and q_field.cache_ability and not args._no_cache_ability then + local ability = v == "BASE" and q_field:get_base_ability(card) or SMODS.get_ability_from_obj(obj, card) + if ability then + SMODS.qfield_cache[card].abilities = SMODS.qfield_cache[card].abilities or {} + table.insert(SMODS.qfield_cache[card].abilities, {t = ability, key = string_key, qfield_key = key}) + end + end + else + SMODS.qfield_cache[card].get[q_field.return_flag][k] = nil + end + end + end + if SMODS.qfield_cache[card].abilities then card:quantum_ability_set_mt() end + return ret +end + + +local function _general_quantum_setter(key, card, value, args, ...) + SMODS.clear_quantum_cache(card) + local qfield_obj = SMODS.QuantumCardFields[key] + local setter_defaults = qfield_obj.setter_defaults + local string_val = value + + local other_args = {...} + if next(other_args) or type(args) ~= "table" then + if key == "edition" then + args = {immediate = args} + args.silent = other_args[1] + args.immediate = args.immediate or args.silent + args.delay = other_args[2] + args.no_juice = args.silent + if type(value) == "table" then + string_val = value.key + value = string_val + end + elseif key == "seal" then + args = {silent = args} + args.immediate = other_args[1] or args.silent + args.no_juice = args.silent + end + end + args = args or {} + args.silent = args.silent or (key == "edition" and SMODS.create_card_silent_edition) + + if card[key] then + card.ability.card_limit = card.ability.card_limit - ((card[key] or {}).card_limit or (card.ability[key] or {}).card_limit or 0) + card.ability.extra_slots_used = card.ability.extra_slots_used - ((card[key] or {}).extra_slots_used or (card.ability[key] or {}).extra_slots_used or 0) + end + + local old_val = card[key] + local new_obj = qfield_obj.g_obj_table[value] + local old_obj = qfield_obj.g_obj_table[old_val] or qfield_obj.g_obj_table[(old_val or {}).key] + if old_obj then + card.ignore_base_shader[old_obj.key] = nil + card.ignore_shadow[old_obj.key] = nil + + if type(old_obj.on_remove) == "function" then + old_obj.on_remove(card) + end + end + + local delay_val, override_val, new_is_base + if new_obj then + delay_val, override_val, new_is_base = qfield_obj:get_setter_values(card, args, old_val, string_val) + if override_val then value = override_val end + end + + card[key] = nil + if not new_is_base and new_obj then + card[key] = value + card.ability[key] = override_val or SMODS.get_ability_from_obj(new_obj, card) + card["delay_" .. key] = delay_val + G.CONTROLLER.locks[key] = true + local sound = not args.silent and (new_obj.sound or setter_defaults.sound) + if args.immediate then + if not args.no_juice then card:juice_up(setter_defaults.juice_scale or 0.3, setter_defaults.juice_rot or 0.3) end + if not args.delay then card["delay_" .. key] = nil end + if sound then play_sound(sound.sound, sound.per, sound.vol) end + G.CONTROLLER.locks[key] = nil + else + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = setter_defaults.delay or 0.3, + func = function() + if not args.no_juice then card:juice_up(setter_defaults.juice_scale or 0.3, setter_defaults.juice_rot or 0.3) end + card["delay_" .. key] = nil + if sound then play_sound(sound.sound, sound.per, sound.vol) end + return true + end + })) + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = setter_defaults.delay_lock or 0.15, + func = function() + G.CONTROLLER.locks[key] = nil + return true + end + })) + end + if args.delay then + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + card["delay_" .. key] = nil + return true + end + })) + end + card.ability.card_limit = card.ability.card_limit + ((card[key] or {}).card_limit or (card.ability[key] or {}).card_limit or 0) + card.ability.extra_slots_used = card.ability.extra_slots_used + ((card[key] or {}).extra_slots_used or (card.ability[key] or {}).extra_slots_used or 0) + + if new_obj.override_base_shader or new_obj.disable_base_shader then + card.ignore_base_shader[string_val] = true + end + if new_obj.no_shadow or new_obj.disable_shadow then + card.ignore_shadow[string_val] = true + end + + if card.area and card.area == G.jokers and key == "edition" then + if card.edition then + if not G.P_CENTERS['e_' .. (card.edition.type)].discovered then + discover_card(G.P_CENTERS['e_' .. (card.edition.type)]) + end + else + if not G.P_CENTERS['e_base'].discovered then + discover_card(G.P_CENTERS['e_base']) + end + end + end + + if type(new_obj.on_apply) == "function" then + new_obj.on_apply(card) + end + else + if not old_obj then return end + local remove_sound = setter_defaults.remove_sound + if not args.silent and remove_sound then + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = not args.immediate and (setter_defaults.delay or 0.3) or 0, + blockable = not args.immediate, + func = function() + if not args.no_juice then card:juice_up(setter_defaults.juice_scale or 0.3, setter_defaults.juice_rot or 0.3) end + play_sound(remove_sound.sound, remove_sound.per, remove_sound.vol) + return true + end + })) + end + if args.delay then + card["delay_" .. key] = old_val + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + card["delay_" .. key] = nil + return true + end + })) + end + return + end + + if card.ability.name == 'Gold Card' and card.seal == 'Gold' and card.playing_card then + check_for_unlock({type = 'double_gold'}) + end + if G.jokers and card.area == G.jokers then + check_for_unlock({ type = 'modify_jokers' }) + end + + card:set_cost() +end + +local function _enhancement_setter_set_ability_redirect(card, ...) + card:set_ability(...) +end + +-- Returns SMODS.qfield_cache[card].has +local function _general_quantum_has_func(card, ...) + if (SMODS.qfield_cache[card] or {}).has then + return SMODS.qfield_cache[card].has -- e.g. {rank = {any = true}, enhancement = {no = true}} + end + _general_quantum_getter(card, ...) + return SMODS.qfield_cache[card].has +end + +function Card:quantum_ability_set_mt() + self._base_ability = self.ability + SMODS.qfield_cache[self].active_ability = self._base_ability + self.ability = setmetatable({}, { + __index = function (t, k) + local active_ability = (SMODS.qfield_cache[self] or {}).active_ability + local active_field = (SMODS.qfield_cache[self] or {}).active_field + if not active_ability then self:quantum_ability_remove_mt(); return self.ability[k] end + if active_field == k then return active_ability end + if active_field == "enhancement" then return active_ability[k] end + return self._base_ability[k] + end, + __newindex = function (t, k, v) + local active_ability = (SMODS.qfield_cache[self] or {}).active_ability + local active_field = (SMODS.qfield_cache[self] or {}).active_field + if not active_ability then self:quantum_ability_remove_mt(); self.ability[k] = v; return end + if active_field == "enhancement" then active_ability[k] = v + else self._base_ability[k] = v end + end, + __pairs = function (t) + local active_ability = (SMODS.qfield_cache[self] or {}).active_ability + if not active_ability then self:quantum_ability_remove_mt(); return next, self.ability, nil end + return next, active_ability, nil + end + }) +end + +function Card:quantum_ability_remove_mt() + if not next(self.ability) then self.ability = self._base_ability end + self._base_ability = nil +end + +function SMODS.get_active_q_ability(abilities, qfield_key, obj_key) + for i, abili_t in ipairs(abilities) do + if abili_t.qfield_key == qfield_key and abili_t.key == obj_key then + return abili_t.t + end + end +end + +function SMODS.set_quantum_cache(card) + _general_quantum_getter(card) + return SMODS.qfield_cache[card] +end + +function SMODS.clear_quantum_cache(card) + if card._base_ability then card:quantum_ability_remove_mt() end + SMODS.qfield_cache[card] = nil +end + +local function _general_quantum_singular_is_func(key, card, value, args, ...) + return SMODS.QuantumCardFields[key].plural_is(card, {[value] = true}, args, ...) +end + +local function _general_quantum_plural_is_func(key, card, values_map, args, ...) + args = args or {} + if card.debuff and not args.bypass_debuff then return false end + local field_values = SMODS.QuantumCardFields[key].getter(card, ...) + + for k, _ in pairs(field_values) do + if values_map[k] then + if not args.all then return true end + elseif args.all then return false end + end + return args.all +end + +local function _general_quantum_tally(key, cards, ...) + if cards.playing_card then + cards = {cards} + end + local tally = {} + local value_to_cards = {} + for _, pcard in ipairs(cards) do + local values = {} + values = SMODS.QuantumCardFields[key].getter(pcard, ...) + for value, t in pairs(values) do + if t then + tally[value] = tally[value] and tally[value] + 1 or 1 + if value_to_cards[value] then value_to_cards[value][pcard] = true + else value_to_cards[value] = {[pcard] = true} end + end + end + end + return tally, value_to_cards +end + +local function _general_quantum_calculate(key, card, context, args, ...) + SMODS.push_to_context_stack(context, card, "quantum_card_fields.lua : _general_quantum_calculate : QField = " .. key) + local q_field = SMODS.QuantumCardFields[key] + SMODS.qfield_cache[card].active_field = key + local values = q_field.getter(card, args, ...) + local ret = {} + for c_key, v in pairs(values) do + local obj = q_field.g_obj_table[c_key] or {} -- Due to Enhancements storing their key equivalently to Jokers (config.center.key), this {} is necessary to prevent a crash when quantum_calculating Jokers. + if obj.calculate and type(obj.calculate) == 'function' then + if SMODS.qfield_cache[card].abilities and q_field.cache_ability then + SMODS.qfield_cache[card].active_ability = SMODS.get_active_q_ability(SMODS.qfield_cache[card].abilities, key, c_key) + end + local o = obj:calculate(card, context) + if o then + if not o.card then o.card = card end + ret[#ret+1] = {o} + end + end + end + SMODS.qfield_cache[card].active_ability = card._base_ability + SMODS.qfield_cache[card].active_field = nil + SMODS.pop_from_context_stack(context, "quantum_card_fields.lua : _general_quantum_calculate : QField = " .. key) + return ret +end + +-- Inject helpers + +local function _quantum_field_inject_getter(args, target_objects) + local getter_func = args.override_getter or function (card, ...) + return _general_quantum_getter(card, ...)[args.key .. "s"] + end + local func_field = "get_" .. args.key .. "s" + for _, target_obj in ipairs(target_objects) do + target_obj[func_field] = getter_func + end + local field_object = SMODS.QuantumCardFields[args.key] + field_object.getter = getter_func +end + +local function _quantum_field_inject_setter(args, target_objects) + local setter_func = args.override_setter or function (card, ...) + return _general_quantum_setter(args.key, card, ...) + end + local func_field = "set_" .. args.key + for _, target_obj in ipairs(target_objects) do + target_obj[func_field] = setter_func + end + local field_object = SMODS.QuantumCardFields[args.key] + field_object.setter = setter_func +end + +local function _quantum_field_inject_has_funcs(args, target_objects) + local has_no_func = args.override_has_no or function (card, ...) + local ret = _general_quantum_has_func(card, ...)[args.key] or {} + return ret.no and not ret.any + end + local has_any_func = args.override_has_any or function (card, ...) + return (_general_quantum_has_func(card, ...)[args.key] or {}).any + end + local has_no_func_field = "has_no_" .. args.key + local has_any_func_field = "has_any_" .. args.key + for _, target_obj in ipairs(target_objects) do + target_obj[has_no_func_field] = has_no_func + target_obj[has_any_func_field] = has_any_func + end + local field_object = SMODS.QuantumCardFields[args.key] + field_object.has_no = has_no_func + field_object.has_any = has_any_func +end + +local function _quantum_field_inject_is_funcs(args, target_objects) + local singular_is_func = function (card, value, ...) + return _general_quantum_singular_is_func(args.key, card, value, ...) + end + local plural_is_func = function (card, values_map, ...) + return _general_quantum_plural_is_func(args.key, card, values_map, ...) + end + local singular_func_field = args.func_prefix .. "_" .. args.key + local plural_func_field = args.func_prefix .. "_" .. args.key .. "s" + for _, target_obj in ipairs(target_objects) do + target_obj[singular_func_field] = singular_is_func + target_obj[plural_func_field] = plural_is_func + end + local field_object = SMODS.QuantumCardFields[args.key] + field_object.singular_is = singular_is_func + field_object.plural_is = plural_is_func +end + +local function _quantum_field_inject_tally(args, target_objects) + local tally_func = args.override_tally or function (cards, ...) + return _general_quantum_tally(args.key, cards, ...) + end + local func_field = "get_" .. args.key .. "_tally" + for _, target_obj in ipairs(target_objects) do + target_obj[func_field] = tally_func + end + local field_object = SMODS.QuantumCardFields[args.key] + field_object.tally = tally_func +end + +local function _quantum_field_inject_calculate(args, target_objects) + local calculate_func = args.override_calculate or function (card, context, ...) + local ret = _general_quantum_calculate(args.key, card, context, ...) + if not ret then return end + return SMODS.merge_effects(unpack(ret)) -- unsure if unpack is deprecated or not + end + local func_field = "calculate_" .. args.key + for _, target_obj in ipairs(target_objects) do + target_obj[func_field] = calculate_func + end + local field_object = SMODS.QuantumCardFields[args.key] + field_object.calculate = calculate_func +end + +-- Class + +SMODS.QuantumCardFields = {} +SMODS.QuantumCardField = SMODS.GameObject:extend { + obj_table = SMODS.QuantumCardFields, + set = 'QuantumCardField', + obj_buffer = {}, + required_params = { + 'key', + 'g_obj_table' + }, + process_loc_text = function() end, + inject = function(self) + local inject_args = self.inject_args or {} + local target_objects = self.target_objects or {} + if not inject_args.no_getter then + target_objects.getter = target_objects.getter or {Card} + _quantum_field_inject_getter({key = self.key, override_getter = self.override_getter}, target_objects.getter) + end + if not inject_args.no_setter then + target_objects.setter = target_objects.setter or {Card} + _quantum_field_inject_setter({key = self.key, override_setter = self.override_setter}, target_objects.setter) + end + if not inject_args.no_has_funcs then + target_objects.has_funcs = target_objects.has_funcs or {SMODS} + _quantum_field_inject_has_funcs({key = self.key, override_has_no = self.override_has_no, override_has_any = self.override_has_any}, target_objects.has_funcs) + end + if not inject_args.no_is_funcs then + target_objects.is_funcs = target_objects.is_funcs or {Card} + _quantum_field_inject_is_funcs({key = self.key, func_prefix = inject_args.is_func_prefix or "is"}, target_objects.is_funcs) + end + if not inject_args.no_tally then + target_objects.tally = target_objects.tally or {SMODS} + _quantum_field_inject_tally({key = self.key, override_tally = self.override_tally}, target_objects.tally) + end + if not inject_args.no_calculate then + target_objects.calculate = target_objects.calculate or {Card} + _quantum_field_inject_calculate({key = self.key, override_calculate = self.override_calculate}, target_objects.calculate) + end + self.get_context_flag = self.get_context_flag or "get_" .. self.key .. "s" + self.has_context_flag = self.has_context_flag or "has_" .. self.key + self.return_flag = self.return_flag or self.key .. "s" + SMODS.CONTEXT_TYPES[self.key] = self.get_context_flag + SMODS.amount_return_flags[self.return_flag] = true + SMODS.amount_return_flags["no_" .. self.key] = true + SMODS.amount_return_flags["any_" .. self.key] = true + table.insert(SMODS.calculation_keys, self.return_flag) + table.insert(SMODS.calculation_keys, "no_" .. self.key) + table.insert(SMODS.calculation_keys, "any_" .. self.key) + if self.default_enabled then SMODS.optional_features.quantum_fields[self.key] = true end + if not self.calc_key then self.calc_key = self.key end + end, + post_inject_class = function(self) + + end, + default_enabled = _default_enabled, + calc_key = nil, + cache_ability = nil, -- Whether the _general_quantum_getter should cache the .config table of this qfield's object values into SMODS.qfield_cache[card].abilities + base_value_ref = nil, -- e.g. 'base.value' for Rank, 'config.center.key' for Enhancement, ... + setter_defaults = {}, -- setter() default values + get_setter_values = function (self, card, args, old_val, new_val) + return (not args.immediate or args.delay) and (old_val or true) or nil, nil, nil + end, + base_ability_ref = "ability", + get_base_ability = function (self, card) return table_get_subfield(card, self.base_ability_ref) end, + get_base_value = function (self, card) return table_get_subfield(card, self.base_value_ref) end, + base_getter = function (self, card, _args) + local base = self:get_base_value(card) + if base then return {[base] = "BASE"} end -- "BASE" is essential for correctly accessing/writing to card.ability when quantum abilities are present. + return {} + end, + getter = nil, -- func defined by inject() + setter = nil, -- func defined by inject() + has_no = nil, -- func defined by inject() + has_any = nil, -- func defined by inject() + singular_is = nil, -- func defined by inject() + plural_is = nil, -- func defined by inject() + calculate = nil, -- func defined by inject() +} + +SMODS.QuantumCardField{ + key = "rank", + g_obj_table = SMODS.Ranks, + base_value_ref = "base.value", + inject_args = { + no_setter = true + }, +} + +SMODS.QuantumCardField{ + key = "enhancement", + g_obj_table = SMODS.Enhancements, + cache_ability = true, + get_context_flag = "check_enhancement", + inject_args = { + is_func_prefix = "has", + }, + override_setter = _enhancement_setter_set_ability_redirect, + target_objects = { + getter = {Card, SMODS}, + is_funcs = {Card, SMODS} + }, + base_value_ref = "config.center.key" +} + +SMODS.QuantumCardField{ + key = "seal", + calc_key = "seals", + cache_ability = true, + g_obj_table = G.P_SEALS, + inject_args = { + is_func_prefix = "has" + }, + base_value_ref = "seal", + base_ability_ref = "ability.seal", + setter_defaults = { + sound = {sound = 'gold_seal', per = 1.2, vol = 0.4}, + }, +} + +SMODS.QuantumCardField{ + key = "edition", + cache_ability = true, + g_obj_table = SMODS.Editions, + base_value_ref = "edition.key", + base_ability_ref = "ability.edition", + setter_defaults = { + remove_sound = {sound = 'whoosh2', per = 1.2, vol = 0.6}, + juice_scale = 1.0, + juice_rot = 0.5, + delay = 0.2, + delay_lock = 0.1 + }, + get_setter_values = function (self, card, args, old_val, new_val) + local delay_val = (not args.immediate or args.delay) and (old_val or true) or nil + local edition_type + if delay_val then delay_val = old_val or {base = true} end + if type(new_val) == 'string' then + assert(string.sub(new_val, 1, 2) == 'e_', ("Edition \"%s\" is missing \"e_\" prefix."):format(new_val)) + edition_type = string.sub(new_val, 3) + elseif type(new_val) == 'table' then + if new_val.type then + edition_type = new_val.type + else + for k, v in pairs(new_val) do + if v then + assert(not edition_type, "Tried to apply more than one edition.") + edition_type = k + end + end + end + end + local override_value = SMODS.get_ability_from_obj(self.g_obj_table[new_val], card) + if edition_type then + override_value[edition_type] = true + override_value.type = edition_type + override_value.key = 'e_'..edition_type + end + return delay_val, override_value, (not edition_type or edition_type == "base") + end +} + +SMODS.QuantumCardField{ + key = "suit", + g_obj_table = SMODS.Suits, + base_value_ref = "base.suit", + base_getter = function (self, card, _args) + local base = self:get_base_value(card) + local smeared = SMODS.smeared_check(card) + if smeared then return smeared end + if base then return {[base] = "BASE"} end + return {} + end, + inject_args = { + no_setter = true + }, +} + +SMODS.QuantumCardField{ + key = "sticker", + g_obj_table = SMODS.Stickers, + inject_args = { + is_func_prefix = "has", + no_setter = true + }, + base_getter = function (self, card, _args) + local stickers = {} + local abilities = SMODS.get_card_abilities(card) + for _, key in ipairs(SMODS.Sticker.obj_buffer) do + for _, ability_t in ipairs(abilities) do + if ability_t.t[key] then -- .t is the actual ability table + stickers[key] = "BASE" + break + end + end + end + return stickers + end, +} + + +SMODS.Seal:take_ownership("Red", { + calculate = function (self, card, context) + if context.repetition then + return { + message = localize('k_again_ex'), + repetitions = 1, + card = card + } + end + end +}) + +SMODS.Seal:take_ownership("Purple", { + calculate = function (self, card, context) + if context.discard and context.other_card == card and #G.consumeables.cards + G.GAME.consumeable_buffer < G.consumeables.config.card_limit then + G.GAME.consumeable_buffer = G.GAME.consumeable_buffer + 1 + G.E_MANAGER:add_event(Event({ + trigger = 'before', + delay = 0.0, + func = (function() + SMODS.add_card({set = "Tarot", area = G.consumeables, key_append = "8ba"}) + G.GAME.consumeable_buffer = 0 + return true + end)})) + card_eval_status_text(card, 'extra', nil, nil, nil, {message = localize('k_plus_tarot'), colour = G.C.PURPLE}) + return nil, true + end + end +}) + +SMODS.Seal:take_ownership("Blue", { + calculate = function (self, card, context) + if context.end_of_round and context.playing_card_end_of_round and context.cardarea == G.hand and #G.consumeables.cards + G.GAME.consumeable_buffer < G.consumeables.config.card_limit then + local card_type = 'Planet' + G.GAME.consumeable_buffer = G.GAME.consumeable_buffer + 1 + G.E_MANAGER:add_event(Event({ + trigger = 'before', + delay = 0.0, + func = (function() + if G.GAME.last_hand_played then + local _planet = 0 + for k, v in pairs(G.P_CENTER_POOLS.Planet) do + if v.config.hand_type == G.GAME.last_hand_played then + _planet = v.key + end + end + if _planet == 0 then _planet = nil end + local card = create_card(card_type,G.consumeables, nil, nil, nil, nil, _planet, 'blusl') + card:add_to_deck() + G.consumeables:emplace(card) + G.GAME.consumeable_buffer = 0 + end + return true + end)})) + card_eval_status_text(card, 'extra', nil, nil, nil, {message = localize('k_plus_planet'), colour = G.C.SECONDARY_SET.Planet}) + return nil, true + end + end +}) + +SMODS.Joker:take_ownership("cloud_9", { + update = function () end, + loc_vars = function (self, info_queue, card) + local tally = SMODS.get_rank_tally(G.playing_cards) + card.ability.nine_tally = tally["9"] or 0 + return { vars = {card.ability.extra, card.ability.extra*(card.ability.nine_tally or 0)}} + end, + calc_dollar_bonus = function (self, card) + local tally = SMODS.get_rank_tally(G.playing_cards) + card.ability.nine_tally = tally["9"] or 0 + if card.ability.nine_tally > 0 then + return card.ability.extra*(card.ability.nine_tally) + end + end +}) + +SMODS.Joker:take_ownership("drivers_license", { + update = function () end, + loc_vars = function (self, info_queue, card) + local tally = 0 + for _, pcard in ipairs(G.playing_cards) do + local enhs = SMODS.get_enhancements(pcard) + if next(enhs) ~= "c_base" or next(enhs, "c_base") then tally = tally + 1 end + end + card.ability.driver_tally = tally + return { vars = {card.ability.extra, card.ability.driver_tally or 0}} + end, + calculate = function (self, card, context) + if context.joker_main then + local tally = 0 + for _, pcard in ipairs(G.playing_cards) do + local enhs = SMODS.get_enhancements(pcard) + if next(enhs) ~= "c_base" or next(enhs, "c_base") then tally = tally + 1 end + end + card.ability.driver_tally = tally + if card.ability.driver_tally >= 16 then + return { + x_mult = card.ability.extra + } + end + end + end +}) + +SMODS.Joker:take_ownership("steel_joker", { + update = function () end, + loc_vars = function (self, info_queue, card) + local tally = SMODS.get_enhancement_tally(G.playing_cards) + card.ability.steel_tally = tally["m_steel"] or 0 + return { vars = {card.ability.extra, 1 + card.ability.extra*(card.ability.steel_tally or 0)}} + end, + calculate = function (self, card, context) + if context.joker_main then + local tally = SMODS.get_enhancement_tally(G.playing_cards) + card.ability.steel_tally = tally["m_steel"] or 0 + if card.ability.steel_tally > 0 then + return { + x_mult = 1 + card.ability.extra*(card.ability.steel_tally) + } + end + end + end +}) + +SMODS.Joker:take_ownership("stone", { + update = function () end, + loc_vars = function (self, info_queue, card) + local tally = SMODS.get_enhancement_tally(G.playing_cards) + card.ability.stone_tally = tally["m_stone"] or 0 + return { vars = {card.ability.extra, card.ability.extra*(card.ability.stone_tally or 0)}} + end, + calculate = function (self, card, context) + if context.joker_main then + local tally = SMODS.get_enhancement_tally(G.playing_cards) + card.ability.stone_tally = tally["m_steel"] or 0 + if card.ability.stone_tally and card.ability.stone_tally > 0 then + return { + chips = card.ability.extra*(card.ability.stone_tally) + } + end + end + end +}) + +SMODS.Joker:take_ownership("flower_pot", { + calculate = function (self, card, context) + if context.joker_main and #context.scoring_hand > 3 then + local suit_tally, value_to_cards = SMODS.get_suit_tally(context.scoring_hand, {bypass_debuff = true}) + local all = suit_tally.Spades and suit_tally.Hearts and suit_tally.Diamonds and suit_tally.Clubs + if all and SMODS.count_bipartite_matching(value_to_cards) > 3 then + return { + x_mult = card.ability.extra + } + end + end + end +}) + +SMODS.Enhancement:take_ownership("m_stone", { + no_rank = true, + no_suit = true, + always_scores = true, +}) + +SMODS.Enhancement:take_ownership("m_wild", { + any_suit = true, +}) + + +-- Todo : Currently the _quantum_getter context is called every time a card is hovered (probably due to SMODS.has_no_suit() for the card name), this causes quite a bit of lag when done (very) rapidly. Consider looking into fixing this. + + +-- Card rank functions +function SMODS.get_rank_from_id(id) + for _, rank in pairs(SMODS.Ranks) do + if rank.id == id then return rank end + end + return nil +end + +function Card:is_parity(parity) + if not self.playing_card then return end + if SMODS.has_any_rank(self, {is_parity_getting_ranks = {parity = parity}}) then + return true + end + for rank, _ in pairs(self:get_ranks({is_parity_getting_ranks = {parity = parity}}, true)) do + if rank.parity == parity then + return true + end + end + return false +end + +function Card:is_royal() + for rank, _ in pairs(self:get_ranks({is_royal_getting_ranks = true}, true)) do + if rank.is_royal then + return true + end + end + return false +end + +function SMODS.all_royal(cards) + if type(cards) ~= "table" then return false end + for _, pcard in ipairs(cards) do + if not pcard:is_royal() then + return false + end + end + return #cards > 0 +end + +function SMODS.lowest_and_highest_rank(cards) + local rank_tally, rank_to_cards = SMODS.get_rank_tally(cards) + local lowest + local highest + for rank, _ in pairs(rank_tally) do + if not lowest or rank.sort_nominal < lowest.sort_nominal then + lowest = {rank = rank, sort_nominal = rank.sort_nominal} + end + if not highest or rank.sort_nominal > highest.sort_nominal then + highest = {rank = rank, sort_nominal = rank.sort_nominal} + end + end + return {lowest = {rank = lowest.rank, cards_map = rank_to_cards[lowest.rank]}, highest = {rank = highest.rank, cards_map = rank_to_cards[highest.rank]}} +end \ No newline at end of file diff --git a/src/game_objects/virtual_ranks.lua b/src/game_objects/virtual_ranks.lua new file mode 100644 index 000000000..ce88b1fac --- /dev/null +++ b/src/game_objects/virtual_ranks.lua @@ -0,0 +1,82 @@ +SMODS.VirtualRanks = {} +SMODS.VirtualRank = SMODS.GameObject:extend { + obj_table = SMODS.VirtualRanks, + obj_buffer = {}, + set = 'VirtualRank', + required_params = { + 'key', + 'base_ranks', + }, + next_redirs = {}, + prev_redirs = {}, + register = function (self) + if self:check_dependencies() then + if SMODS.Ranks[self.key] then + sendWarnMessage(("VirtualRank from '%s' tried to register with existing Rank key '%s'"):format(self.mod.name, self.key), self.set) + return + end + self.obj_table[self.key] = self + self.obj_buffer[#self.obj_buffer + 1] = self.key + self.registered = true + end + end, + inject = function(self) + for rank, _ in pairs(self.base_ranks) do + if SMODS.Ranks[rank] then + table.insert(SMODS.Ranks[rank].virtual.ranks, self.key) + else + self.base_ranks[rank] = nil + sendWarnMessage(("VirtualRank from '%s' tried to register with invalid base_rank key '%s'"):format(self.mod.name, self.key), self.set) + end + end + for _, dir in ipairs({"next", "prev"}) do + for redir_rank, do_redir in pairs(self[dir.."_redirs"]) do + if SMODS.Ranks[redir_rank] then + SMODS.Ranks[redir_rank].virtual[dir] = SMODS.Ranks[redir_rank].virtual[dir] or {} + if do_redir then + SMODS.Ranks[redir_rank].virtual[dir][self.key] = true + else + -- Do nothin' because the line before the if statement already guarantees overriding of base .next/.prev + end + end + end + end + end, + delete = function(self) + local i + for j, v in ipairs(self.obj_buffer) do + if v == self.key then i = j end + end + table.remove(self.obj_buffer, i) + end, + get_straight_next = function (self, direction, do_wrap) + local dir = direction == "prev" and "prev" or "next" + local ret = {} + for k, v in pairs(self[dir] or {}) do + ret[k] = v + end + if do_wrap then + for k, v in pairs(self[dir.."_wrap"] or {}) do + ret[k] = v + end + end + return ret + end +} + +SMODS.VirtualRank { + key = "Ace_low", + base_ranks = {Ace = true}, + next = {['2'] = true}, + prev_redirs = {['2'] = true}, + -- next_redirs = {King = false}, -- Technically redundant because of the Ace_high overriding it + prev_wrap = {King = true}, +} +SMODS.VirtualRank { + key = "Ace_high", + base_ranks = {Ace = true}, + prev = {King = true}, + next_redirs = {King = true}, + -- prev_redirs = {['2'] = false}, -- Technically redundant because of the Ace_low overriding it + next_wrap = {['2'] = true}, +} \ No newline at end of file diff --git a/src/overrides.lua b/src/overrides.lua index f735f75c9..1aaeb625c 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -619,74 +619,382 @@ G.FUNCS.change_stake = function(args) end --#endregion ---#region straights and view deck UI +--#region Flush, Straights, X same, quantum Full House and Two Pair + +function get_flush(hand) + local ret = {} + local four_fingers = SMODS.four_fingers('flush') + if #hand < four_fingers then return ret end + local suit_tally, value_to_cards = SMODS.get_suit_tally(hand) + for k, tally in pairs(suit_tally) do + if tally >= four_fingers then + local t = SMODS.get_sorted_card_list(value_to_cards[k]) + table.insert(ret, t) + return ret -- Vanilla early return + end + end + return ret +end function get_straight(hand, min_length, skip, wrap) min_length = min_length or 5 if min_length < 2 then min_length = 2 end if #hand < min_length then return {} end - local ranks = {} - for k,_ in pairs(SMODS.Ranks) do ranks[k] = {} end - for _,card in ipairs(hand) do - local id = card:get_id() - if id > 0 then - for k,v in pairs(SMODS.Ranks) do - if v.id == id then table.insert(ranks[k], card); break end - end - end - end - local function next_ranks(key, start) - local rank = SMODS.Ranks[key] - local ret = {} - if not start and not wrap and rank.straight_edge then return ret end - for _,v in ipairs(rank.next) do - ret[#ret+1] = v - if skip and (wrap or not SMODS.Ranks[v].straight_edge) then - for _,w in ipairs(SMODS.Ranks[v].next) do - ret[#ret+1] = w - end - end - end - return ret - end - local tuples = {} - local ret = {} - for _,k in ipairs(SMODS.Rank.obj_buffer) do - if next(ranks[k]) then - tuples[#tuples+1] = {k} - end - end - for i = 2, #hand+1 do - local new_tuples = {} - for _, tuple in ipairs(tuples) do - local any_tuple - if i ~= #hand+1 then - for _,l in ipairs(next_ranks(tuple[i-1], i == 2)) do - if next(ranks[l]) then - local new_tuple = {} - for _,v in ipairs(tuple) do new_tuple[#new_tuple+1] = v end - new_tuple[#new_tuple+1] = l - new_tuples[#new_tuples+1] = new_tuple - any_tuple = true - end - end - end - if i > min_length and not any_tuple then - local straight = {} - for _,v in ipairs(tuple) do - for _,card in ipairs(ranks[v]) do - straight[#straight+1] = card - end - end - ret[#ret+1] = straight - end - end - tuples = new_tuples - end - table.sort(ret, function(a,b) return #a > #b end) - return ret + local ranks = SMODS.get_straight_ranks() + + -- Card Representer Struct + -- This is placed into [ranks] at every rank its card may be. + local CardRep = { + new = function (card, av_ranks) + local cr = { + card = card, + ranks = av_ranks -- Map of ranks or "WILD" if Wild Rank + } + return cr + end + } + + -- All card_rep instances + local card_reps = {} + + local wild_reps = {} + local wild_rank_count = 0 + + for _, pcard in ipairs(hand) do + if SMODS.has_any_rank(pcard, {eval_getting_ranks = {type = "straight"}}) then + local card_rep = CardRep.new(pcard, "WILD") + card_reps[#card_reps+1] = card_rep + wild_reps[#wild_reps+1] = card_rep + wild_rank_count = wild_rank_count + 1 + else + local pcard_ranks = SMODS.get_straight_ranks(pcard:get_ranks({as_objs = true}), true) + + if next(pcard_ranks) then + local card_rep = CardRep.new(pcard, pcard_ranks) + card_reps[#card_reps+1] = card_rep -- Add card_rep to card_reps_holder + + for p_rank, _ in pairs(pcard_ranks) do + ranks[p_rank.key][card_rep] = true -- Add card_rep to rank + end + end + end + end + + -- TODO: Optimization that may or may not be worth it: + -- Remove all card_reps that share a rank with a 1-rank card_rep at that rank. Repeat until none are removed. + -- Also, collapse any N card_reps that share N ranks into one of those ranks each. (Hard to implement / too inefficient maybe) + + -- Recursive function to get all .nexts/.prevs of a rank and its .next/.prev ranks down to a [depth] (which should be the shortcut level), merged into one table + -- [depth] = 0 is base, 1 is shortcut + local get_next_ranks + get_next_ranks = function (rank, depth, do_wrap, direction) + direction = direction or "next" + local rec_ret = {} + for r, _ in pairs(rank:get_straight_next(direction, do_wrap)) do + rec_ret[#rec_ret+1] = SMODS.Ranks[r] or SMODS.VirtualRanks[r] + if depth > 0 then + for v, _ in pairs(get_next_ranks(SMODS.Ranks[r] or SMODS.VirtualRanks[r], depth - 1, do_wrap, direction)) do + rec_ret[#rec_ret+1] = v + end + end + end + return rec_ret + end + + -- Recursive function to find a valid straight, with [rank] as the starting point + -- This function goes both ways; .next and .prev + -- When called with [direction] = nil, the function first sets the [current_straight] to the best straight from a [direction] = "prev_base" call to itself + -- and then uses that as a base recursively with [direction] = "next_base" + local recursive_get_straight + recursive_get_straight = function (rank, other_rank, current_straight, max_skips, do_wrap, direction, used_c_reps, used_wilds) + local next_straight = {} + for k, v in pairs(current_straight) do next_straight[k] = v end + local ret_straight = next_straight + + -- Check and setup c_reps for the current rank + local c_reps = ranks[rank.key] + if not c_reps or not next(c_reps) then + if used_wilds < wild_rank_count then + used_wilds = used_wilds + 1 + c_reps = {[wild_reps[used_wilds]] = "WILD"} + else + return current_straight -- If no card_representers are present for the given [rank] and no Wild Ranks remain, return whatever [current_straight] was passed to the function + end + end + + -- Get all next ranks for this rank + local rank_nexts = get_next_ranks(rank, max_skips, do_wrap, direction) + + for c_rep, exists in pairs(c_reps) do + if exists and not used_c_reps[c_rep] then -- If the encountered card_rep hasn't assumed a rank yet + if exists ~= "WILD" then used_c_reps[c_rep] = true end + next_straight[#next_straight+1] = {c_rep=c_rep, rank=exists ~= "WILD" and rank} + ret_straight = next_straight + local all_dead_ends = true + for _, n_rank in ipairs(rank_nexts) do + local rec_ret_straight = recursive_get_straight(n_rank, other_rank, next_straight, max_skips, do_wrap, direction, used_c_reps, used_wilds) + if rec_ret_straight ~= next_straight then + all_dead_ends = false + end + if #rec_ret_straight > #ret_straight then + ret_straight = rec_ret_straight + end + if #ret_straight >= #hand then + return ret_straight + end + end + if other_rank and all_dead_ends then -- If it hit a dead end, try to extend backwards from beginning + ret_straight = recursive_get_straight(other_rank, nil, next_straight, max_skips, do_wrap, "prev", used_c_reps, used_wilds - (exists == "WILD" and 1 or 0)) -- If the current c_rep was a wild_rep, reduce used_wilds by one, so that it may instead be used to extend the prev part of the straight. + end + if SMODS.optional_features.quantum_straight_min_return and #ret_straight > min_length then return ret_straight end -- Greatly decreases calculation time but doesn't ensure that the longest straight is found. + next_straight = {} + for k, v in pairs(current_straight) do next_straight[k] = v end + used_c_reps[c_rep] = false + end + end + + return ret_straight + end + + + -- Alright now we can evaluate the straight :) + + local max_hole_size = skip and 1 or 0 + local best_straight = {} + local branches_checked = 0 + + local appeared_in_ret_straight = {} -- {rank: boolean} + + -- Iterate over all card_reps and all their ranks as starting points of straight calculation, however: + -- ranks that have appeared in a returned straight are no longer used as starting points, because they cannot yield a new straight. + -- (Even if their .next/.prev forks, because then the fork's ranks will yield the fork's straight later on) + for _, c_rep in ipairs(card_reps) do + if c_rep.ranks ~= "WILD" then + local used_c_reps = {[c_rep] = true} -- This is a set of all card_reps used during this eval. They are set in recursive_get_straight() + local had_new_rank = false + for rank, _ in pairs(c_rep.ranks) do + if not appeared_in_ret_straight[rank] then + local current_straight = {{c_rep=c_rep, rank=rank}} + had_new_rank = true + for _, next_rank in ipairs(get_next_ranks(rank, max_hole_size, wrap, "next")) do + for _, prev_rank in ipairs(get_next_ranks(rank, max_hole_size, wrap, "prev")) do + local ret_straight = recursive_get_straight(next_rank, prev_rank, current_straight, max_hole_size, wrap, "next", used_c_reps, 0) + + if #ret_straight >= min_length and #ret_straight > #best_straight then + best_straight = ret_straight + end + + for _, tup in ipairs(ret_straight) do + if tup.rank then + appeared_in_ret_straight[tup.rank] = true + end + end + -- Optimization: If the size of the played hand minus the minimum straight length (5=default, 4=Four Fingers) + -- minus the amount of branches checked (unique returned straights) is less than zero, there cannot be a straight in the hand. + -- (Equally, if the length of the best_straight is more or equal the length of the hand minus one, the remaining card cannot result in a longer straight (it must be a fork in .next/.prev)) + if ( + #hand - min_length - branches_checked < 0 or + #best_straight >= #hand - 1 or + (SMODS.optional_features.quantum_straight_min_return and #best_straight > min_length) + ) then + goto continue + end + end + end + end + end + if had_new_rank then branches_checked = branches_checked + 1 end + end + end + + ::continue:: + + if #best_straight >= min_length then + for k, v in ipairs(best_straight) do best_straight[k] = v.c_rep.card end + return {best_straight} -- Only return best found straight. Feasible to return all found straights, but I need sleep :) + elseif #wild_reps >= min_length then + for i, c_rep in ipairs(wild_reps) do best_straight[i] = c_rep.card end + return {best_straight} + end + + return {} +end + + +function get_X_same(num, hand, or_more) + if not SMODS.optional_features.quantum_fields.rank then + local vals = {} + for i = 1, SMODS.Rank.max_id.value do + vals[i] = {} + end + for i=#hand, 1, -1 do + local curr = {} + table.insert(curr, hand[i]) + for j=1, #hand do + if hand[i]:get_id() == hand[j]:get_id() and i ~= j then + table.insert(curr, hand[j]) + end + end + if or_more and (#curr >= num) or (#curr == num) then + vals[curr[1]:get_id()] = curr + end + end + local ret = {} + for i=#vals, 1, -1 do + if next(vals[i]) then table.insert(ret, vals[i]) end + end + return ret + else + local rank_tally, rank_to_cards = SMODS.get_rank_tally(hand, {eval_getting_ranks = {type = "x_same", x_same = num, or_more = or_more}}) + + local ret = {} + + for rank, tally in pairs(rank_tally) do + if or_more and (tally >= num) or (tally == num) then + local rank_cards = SMODS.get_sorted_card_list(rank_to_cards[rank]) + table.insert(ret, rank_cards) + end + end + return ret + end +end + + +-- Function to get valid quantum pairings of X sets of cards, like Two Pair (2 + 2) and Full House (2 + 3) +-- [parts] expects the following structure; { {pairs = [pokerhandpart], min_len = [number]}, ... } +-- Example Full House (from game_object.lua): parts = {{pairs = parts._2, min_len = 2}, {pairs = parts._3, min_len = 3}} +function get_quantum_pairing(parts, base) + if not SMODS.optional_features.quantum_fields.rank then return {} end + if #parts < 2 and not base then return {} end + + local parts_list = {} + for i, v in ipairs(parts) do + parts_list[i] = v + end + + local valid_pairs = {} + local used_cards = {} + local used_pairs = {} + if base then + for _, _pairs in ipairs(base) do + for pair, over_by in pairs(_pairs) do + used_pairs[pair] = over_by + for _, pcard in ipairs(pair) do + used_cards[pcard] = pair + end + end + end + end + + if base then + for _, pair in ipairs(parts_list[#parts_list].pairs) do + if not used_pairs[pair] then + local pair_over_by = #pair - parts_list[#parts_list].min_len + local valid = true + local overlap_decreases = {} + for _, pcard in ipairs(pair) do + local overlap = used_cards[pcard] -- The pair in which the card appeared before + if overlap then + overlap_decreases[overlap] = overlap_decreases[overlap] or 0 + if pair_over_by + (used_pairs[overlap] - overlap_decreases[overlap]) <= 0 then + valid = false + break + else + if pair_over_by > 0 then + pair_over_by = pair_over_by - 1 + else + overlap_decreases[overlap] = overlap_decreases[overlap] + 1 + end + end + end + end + if valid then + local extension = {} + for b_pair, over_by in pairs(base) do + extension[b_pair] = over_by - (overlap_decreases[b_pair] or 0) + end + extension[pair] = pair_over_by + valid_pairs[#valid_pairs+1] = extension + end + end + end + else + for _, pair_1 in ipairs(parts_list[#parts_list].pairs) do + if not used_pairs[pair_1] then + local pair_1_over_by = #pair_1 - parts_list[#parts_list].min_len + for _, pcard in ipairs(pair_1) do + used_cards[pcard] = pair_1 + end + for _, pair_2 in ipairs(parts_list[#parts_list-1].pairs) do + if not used_pairs[pair_2] and pair_1 ~= pair_2 then + local pair_2_over_by = #pair_2 - parts_list[#parts_list-1].min_len + local valid = true + local overlap_decrease = 0 + for _, pcard in ipairs(pair_2) do + local overlap = used_cards[pcard] + if overlap then + if pair_2_over_by + (pair_1_over_by - overlap_decrease) <= 0 then + valid = false + break + else + if pair_2_over_by > 0 then + pair_2_over_by = pair_2_over_by - 1 + else + overlap_decrease = overlap_decrease + 1 + end + end + end + end + if valid then + valid_pairs[#valid_pairs+1] = {[pair_1] = pair_1_over_by - overlap_decrease, [pair_2] = pair_2_over_by} + used_pairs[pair_1] = true -- true because over_by isn't used here before recursion, only needs pair_1 because that already fixes the symmetry issue (When passing the same parts twice, would iterate over pairing AB and also BA) + end + end + end + end + end + end + + if #parts_list > 2 and not base then -- If first call and there's more than 2 parts + table.remove(parts_list, #parts_list) + table.remove(parts_list, #parts_list) + for _, valid_pair in ipairs(valid_pairs) do + local valid_hand = get_quantum_pairing(parts_list, valid_pair) + if valid_hand then + return valid_hand + end + end + elseif #parts_list > 1 and base then -- Else if there's more than 1 part remaining and it's during recursion + table.remove(parts_list, #parts_list) + for _, valid_pair in ipairs(valid_pairs) do + local valid_hand = get_quantum_pairing(parts_list, valid_pair) + if valid_hand then + return valid_hand + end + end + return nil + end + + if next(valid_pairs) then + local card_map = {} + local merged_cards = {} + for pair, _ in pairs(valid_pairs[1]) do + for _, pcard in ipairs(pair) do + card_map[pcard] = true + end + end + for pcard, _ in pairs(card_map) do + merged_cards[#merged_cards+1] = pcard + end + return { merged_cards } + end + + return nil end +--#endregion +--#region view deck UI + function G.UIDEF.deck_preview(args) local _minh, _minw = 0.35, 0.5 local suit_labels = {} @@ -1586,12 +1894,7 @@ function G.FUNCS.get_poker_hand_info(_cards) disp_text = text local _hand = SMODS.PokerHands[text] if text == 'Straight Flush' then - local royal = true - for j = 1, #scoring_hand do - local rank = SMODS.Ranks[scoring_hand[j].base.value] - royal = royal and (rank.key == 'Ace' or rank.key == '10' or rank.face) - end - if royal then + if SMODS.all_royal(scoring_hand) then disp_text = 'Royal Flush' end elseif _hand and _hand.modify_display_text and type(_hand.modify_display_text) == 'function' then @@ -2063,168 +2366,6 @@ function Card:load(cardTable, other_card) return ret end --- self = pass the card --- edition = --- nil (removes edition) --- OR key as string --- OR { name_of_edition = true } (key without e_). This is from the base game, prefer using a string. --- OR another card's self.edition table --- immediate = boolean value --- silent = boolean value -function Card:set_edition(edition, immediate, silent, delay) - SMODS.enh_cache:write(self, nil) - - silent = silent or SMODS.create_card_silent_edition - - if self.edition then - 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 - - local old_edition = self.edition - if old_edition and old_edition.key then - self.ignore_base_shader[old_edition.key] = nil - self.ignore_shadow[old_edition.key] = nil - - local on_old_edition_removed = G.P_CENTERS[old_edition.key] and G.P_CENTERS[old_edition.key].on_remove - if type(on_old_edition_removed) == "function" then - on_old_edition_removed(self) - end - end - - local edition_type = nil - if type(edition) == 'string' then - assert(string.sub(edition, 1, 2) == 'e_', ("Edition \"%s\" is missing \"e_\" prefix."):format(edition)) - edition_type = string.sub(edition, 3) - elseif type(edition) == 'table' then - if edition.type then - edition_type = edition.type - else - for k, v in pairs(edition) do - if v then - assert(not edition_type, "Tried to apply more than one edition.") - edition_type = k - end - end - end - end - - if not edition_type or edition_type == 'base' then - if self.edition == nil then -- early exit - return - end - self.edition = nil -- remove edition from card - self:set_cost() - if not silent then - G.E_MANAGER:add_event(Event({ - trigger = 'after', - delay = not immediate and 0.2 or 0, - blockable = not immediate, - func = function() - self:juice_up(1, 0.5) - play_sound('whoosh2', 1.2, 0.6) - return true - end - })) - end - if delay then - self.delay_edition = old_edition - G.E_MANAGER:add_event(Event({ - trigger = 'immediate', - func = function() - self.delay_edition = nil - return true - end - })) - end - return - end - - self.edition = {} - self.edition[edition_type] = true - self.edition.type = edition_type - self.edition.key = 'e_' .. edition_type - - local p_edition = G.P_CENTERS['e_' .. edition_type] - - if p_edition.override_base_shader or p_edition.disable_base_shader then - self.ignore_base_shader[self.edition.key] = true - end - if p_edition.no_shadow or p_edition.disable_shadow then - self.ignore_shadow[self.edition.key] = true - end - - for k, v in pairs(p_edition.config) do - if type(v) == 'table' then - self.edition[k] = copy_table(v) - else - self.edition[k] = v - end - end - - local on_edition_applied = p_edition.on_apply - if type(on_edition_applied) == "function" then - on_edition_applied(self) - end - - if self.area and self.area == G.jokers then - if self.edition then - if not G.P_CENTERS['e_' .. (self.edition.type)].discovered then - discover_card(G.P_CENTERS['e_' .. (self.edition.type)]) - end - else - if not G.P_CENTERS['e_base'].discovered then - discover_card(G.P_CENTERS['e_base']) - end - end - end - - if self.edition and not silent then - local ed = G.P_CENTERS['e_' .. (self.edition.type)] - G.CONTROLLER.locks.edition = true - G.E_MANAGER:add_event(Event({ - trigger = 'after', - delay = not immediate and 0.2 or 0, - blockable = not immediate, - func = function() - if self.edition then - self:juice_up(1, 0.5) - play_sound(ed.sound.sound, ed.sound.per, ed.sound.vol) - end - return true - end - })) - G.E_MANAGER:add_event(Event({ - trigger = 'after', - delay = 0.1, - func = function() - G.CONTROLLER.locks.edition = false - return true - end - })) - end - - if delay then - self.delay_edition = old_edition or {base = true} - G.E_MANAGER:add_event(Event({ - trigger = 'immediate', - func = function() - self.delay_edition = nil - return true - end - })) - end - - 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) - - - if G.jokers and self.area == G.jokers then - check_for_unlock({ type = 'modify_jokers' }) - end - - self:set_cost() -end -- _key = key value for random seed -- _mod = scale of chance against base card (does not change guaranteed weights) @@ -2685,6 +2826,28 @@ function Card:use_consumeable(area, copier) return ret end +function Card:is_face(from_boss) + if self.debuff and not from_boss then return end + if not SMODS.optional_features.quantum_fields.rank then + local id = self:get_id() + local rank = SMODS.Ranks[self.base.value] + if not id then return end + if (id > 0 and rank and rank.face) or next(find_joker("Pareidolia")) then + return true + end + else + if next(find_joker("Pareidolia")) then return true end + if SMODS.has_any_rank(self) then + return true + end + + for rank, _ in pairs(self:get_ranks({as_objs = true})) do + if rank.face then return true end + end + end + return false +end + local ease_ante_ref = ease_ante function ease_ante(mod) local flags = SMODS.calculate_context({modify_ante = mod, ante_end = SMODS.ante_end}) @@ -2696,19 +2859,12 @@ end local eval_card_ref = eval_card function eval_card(card, context) SMODS.push_to_context_stack(context, card, "overrides.lua : eval_card") + SMODS.set_quantum_cache(card) local eff, post = eval_card_ref(card, context) SMODS.pop_from_context_stack(context, "overrides.lua : eval_card") return eff, post end -local calculate_seal_ref = Card.calculate_seal -function Card:calculate_seal(context, ...) - SMODS.push_to_context_stack(context, self, "overrides.lua : Card.calculate_seal") - local eff, post = calculate_seal_ref(self, context, ...) - SMODS.pop_from_context_stack(context, "overrides.lua : Card.calculate_seal") - return eff, post -end - local calculate_joker_ref = Card.calculate_joker function Card:calculate_joker(context, ...) SMODS.push_to_context_stack(context, self, "overrides.lua : Card.calculate_joker") @@ -2719,11 +2875,11 @@ end local set_ability = Card.set_ability function Card:set_ability(center, initial, delay_sprites) - if delay_sprites == "quantum" then return self:quantum_set_ability(center) end local old_center = self.config.center if delay_sprites and not initial then self.delay_center = old_center end + SMODS.clear_quantum_cache(self) set_ability(self, 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}) @@ -2742,151 +2898,6 @@ function add_tag(_tag) add_tag_ref(_tag) end -function Card:quantum_set_ability(center) - SMODS.enh_cache:write(self, nil) - - if self.ability then - self.ability.card_limit = self.ability.card_limit - (self.config.center.config.card_limit or 0) - self.ability.extra_slots_used = self.ability.extra_slots_used - (self.config.center.config.extra_slots_used or 0) - end - - local old_center = self.config.center - self.from_quantum = true - if type(center) == 'string' then - assert(G.P_CENTERS[center], ("Could not find center \"%s\""):format(center)) - center = G.P_CENTERS[center] - end - self.config.center = center - if self.config.center.key then - self.config.center_key = self.config.center.key - else - for k, v in pairs(G.P_CENTERS) do - if center == v then self.config.center_key = k end - end - end - - if self.ability and old_center and old_center.config.bonus then - self.ability.bonus = self.ability.bonus - old_center.config.bonus - end - - self.ARGS.smods_quantum_ability = self.ARGS.smods_quantum_ability or {} - local new_ability = self.ARGS.smods_quantum_ability - - new_ability.name = center.name - new_ability.effect = center.effect - new_ability.set = center.set - new_ability.mult = center.config.mult or 0 - new_ability.h_mult = center.config.h_mult or 0 - new_ability.h_x_mult = center.config.h_x_mult or 0 - new_ability.h_dollars = center.config.h_dollars or 0 - new_ability.p_dollars = center.config.p_dollars or 0 - new_ability.t_mult = center.config.t_mult or 0 - new_ability.t_chips = center.config.t_chips or 0 - new_ability.x_mult = center.config.Xmult or center.config.x_mult or 1 - new_ability.h_chips = center.config.h_chips or 0 - new_ability.x_chips = center.config.x_chips or 1 - new_ability.h_x_chips = center.config.h_x_chips or 1 - new_ability.repetitions = center.config.repetitions or 0 - new_ability.h_size = center.config.h_size or 0 - new_ability.d_size = center.config.d_size or 0 - new_ability.extra = copy_table(center.config.extra) or nil - new_ability.extra_value = 0 - new_ability.type = center.config.type or '' - new_ability.order = center.order or nil - new_ability.forced_selection = self.ability and self.ability.forced_selection or nil - new_ability.perma_bonus = self.ability and self.ability.perma_bonus or 0 - new_ability.perma_x_chips = self.ability and self.ability.perma_x_chips or 0 - new_ability.perma_mult = self.ability and self.ability.perma_mult or 0 - new_ability.perma_x_mult = self.ability and self.ability.perma_x_mult or 0 - new_ability.perma_h_chips = self.ability and self.ability.perma_h_chips or 0 - new_ability.perma_h_x_chips = self.ability and self.ability.perma_h_x_chips or 0 - new_ability.perma_h_mult = self.ability and self.ability.perma_h_mult or 0 - new_ability.perma_h_x_mult = self.ability and self.ability.perma_h_x_mult or 0 - new_ability.perma_p_dollars = self.ability and self.ability.perma_p_dollars or 0 - new_ability.perma_h_dollars = self.ability and self.ability.perma_h_dollars or 0 - new_ability.perma_repetitions = self.ability and self.ability.perma_repetitions or 0 - new_ability.card_limit = self.ability and self.ability.card_limit or 0 - new_ability.extra_slots_used = self.ability and self.ability.extra_slots_used or 0 - new_ability.perma_score = self.ability and self.ability.perma_score or 0 - new_ability.perma_h_score = self.ability and self.ability.perma_h_score or 0 - new_ability.perma_x_score = self.ability and self.ability.perma_x_score or 0 - new_ability.perma_h_x_score = self.ability and self.ability.perma_h_x_score or 0 - new_ability.perma_blind_size = self.ability and self.ability.perma_blind_size or 0 - new_ability.perma_h_blind_size = self.ability and self.ability.perma_h_blind_size or 0 - new_ability.perma_x_blind_size = self.ability and self.ability.perma_x_blind_size or 0 - new_ability.perma_h_x_blind_size = self.ability and self.ability.perma_h_x_blind_size or 0 - - self.ability = self.ability or {} - new_ability.extra_value = nil - new_ability.debuff_sources = {} - self.ability.extra_value = self.ability.extra_value or 0 - for k, v in pairs(new_ability) do - self.ability[k] = v - end - - -- handles card_limit/extra_slots_used changes - self.ability.card_limit = self.ability.card_limit + (center.config.card_limit or 0) - self.ability.extra_slots_used = self.ability.extra_slots_used + (center.config.extra_slots_used or 0) - - - -- reset keys do not persist on ability change - for _, k in ipairs(SMODS.get_ability_reset_keys(self) or {}) do - self.ability[k] = new_ability[k] - end - - self.ability.bonus = (self.ability.bonus or 0) + (center.config.bonus or 0) - if not self.ability.name then self.ability.name = center.key end - for k, v in pairs(center.config) do - if k ~= 'bonus' then - if type(v) == 'table' then - self.ability[k] = copy_table(v) - else - self.ability[k] = v - end - end - end - - if center.consumeable then - self.ability.consumeable = center.config - end - - if self.ability.name == "Invisible Joker" then - self.ability.invis_rounds = 0 - end - if self.ability.name == 'To Do List' then - local _poker_hands = {} - for k, v in pairs(G.GAME.hands) do - if SMODS.is_poker_hand_visible(k) then _poker_hands[#_poker_hands+1] = k end - end - local old_hand = self.ability.to_do_poker_hand - self.ability.to_do_poker_hand = nil - - while not self.ability.to_do_poker_hand do - self.ability.to_do_poker_hand = pseudorandom_element(_poker_hands, pseudoseed((self.area and self.area.config.type == 'title') and 'false_to_do' or 'to_do')) - if self.ability.to_do_poker_hand == old_hand then self.ability.to_do_poker_hand = nil end - end - end - if self.ability.name == 'Caino' then - self.ability.caino_xmult = 1 - end - if self.ability.name == 'Yorick' then - self.ability.yorick_discards = self.ability.extra.discards - end - if self.ability.name == 'Loyalty Card' then - self.ability.burnt_hand = 0 - self.ability.loyalty_remaining = self.ability.extra.every - end - - self.ability.hands_played_at_create = G.GAME and G.GAME.hands_played or 0 - - local obj = self.config.center - if obj.set_ability and type(obj.set_ability) == 'function' then - obj:set_ability(self, false, nil) - end - - EMPTY(new_ability) - self.from_quantum = nil -end -- Fix visual glitch in deck select local g_funcs_change_viewed_back_ref = G.FUNCS.change_viewed_back G.FUNCS.change_viewed_back = function(...) @@ -2978,6 +2989,11 @@ end end +function Card:get_end_of_round_effect(context) + local ret = {} + return ret +end + -- AnimatedSprite : Use obj.sprite_args and allow wrapping / overlapping frames / StateSprite args like flipped_h/v, frame_duration(s) and frame_order. function AnimatedSprite:init(X, Y, W, H, new_sprite_atlas, sprite_pos, args) sprite_pos = sprite_pos or {x=0, y=0} diff --git a/src/utils.lua b/src/utils.lua index 7baf341e2..4888b5e8b 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -316,35 +316,33 @@ function SMODS.modify_rank(card, amount, manual_sprites) local rank_data = SMODS.Ranks[card.base.value] if amount > 0 then for _ = 1, amount do - local behavior = rank_data.strength_effect or { fixed = 1, ignore = false, random = false } + local behavior = rank_data.strength_effect or { fixed_next = false, ignore = false, random = false } if behavior.ignore or not next(rank_data.next) then break elseif behavior.random then rank_key = pseudorandom_element( - rank_data.next, + SMODS.get_keys(rank_data.next), pseudoseed('strength'), { in_pool = function(key) return SMODS.add_to_pool(SMODS.Ranks[key], { suit = card.base.suit }) end } ) else - local i = (behavior.fixed and rank_data.next[behavior.fixed]) and behavior.fixed or 1 - rank_key = rank_data.next[i] + rank_key = behavior.fixed_next or SMODS.get_keys(rank_data.next)[1] end rank_data = SMODS.Ranks[rank_key] end else for _ = 1, -amount do - local behavior = rank_data.prev_behavior or { fixed = 1, ignore = false, random = false } + local behavior = rank_data.prev_behavior or { fixed_prev = false, ignore = false, random = false } if not next(rank_data.prev) or behavior.ignore then break elseif behavior.random then rank_key = pseudorandom_element( - rank_data.prev, + SMODS.get_keys(rank_data.prev), pseudoseed('weakness'), { in_pool = function(key) return SMODS.add_to_pool(SMODS.Ranks[key], { suit = card.base.suit }) end } ) else - local i = (behavior.fixed and rank_data.prev[behavior.fixed]) and behavior.fixed or 1 - rank_key = rank_data.prev[i] + rank_key = behavior.fixed_prev or SMODS.get_keys(rank_data.prev)[1] end rank_data = SMODS.Ranks[rank_key] end @@ -428,7 +426,7 @@ function SMODS.create_card(t) -- Should this be restricted to only cards able to handle these -- or should that be left to the person calling SMODS.create_card to use it correctly? if t.edition then _card:set_edition(t.edition, nil, t.silent.edition) end - if t.seal then _card:set_seal(t.seal, t.silent.seal); _card.ability.delay_seal = false end + if t.seal then _card:set_seal(t.seal, t.silent.seal); _card.delay_seal = nil end if t.stickers or type(t.force_stickers) == "table" then local applied_stickers = {} if type(t.force_stickers) == "table" then @@ -985,26 +983,12 @@ function Card:add_sticker(sticker, bypass_check) local sticker = SMODS.Stickers[sticker] if bypass_check or (sticker and sticker.should_apply and type(sticker.should_apply) == 'function' and sticker:should_apply(self, self.config.center, self.area, true)) then sticker:apply(self, true) - SMODS.enh_cache:write(self, nil) end end function Card:remove_sticker(sticker) if (sticker == 'pinned' and self.pinned) or self.ability[sticker] then SMODS.Stickers[sticker]:apply(self, false) - SMODS.enh_cache:write(self, nil) - end -end - - -function Card:calculate_sticker(context, key) - local sticker = SMODS.Stickers[key] - if self.ability[key] and type(sticker.calculate) == 'function' then - local o = sticker:calculate(self, context) - if o then - if not o.card then o.card = self end - return o - end end end @@ -1014,78 +998,19 @@ function Card:can_calculate(ignore_debuff, ignore_sliced) return is_available end -function Card:calculate_enhancement(context) - if self.ability.set ~= 'Enhanced' then return nil end - local center = self.config.center - if center.calculate and type(center.calculate) == 'function' then - local o = center:calculate(self, context) - if o then - if not o.card then o.card = self end - return o - end - end -end - -function SMODS.get_enhancements(card, extra_only) - if not SMODS.optional_features.quantum_enhancements or not G.hand then - return not extra_only and card.ability.set == 'Enhanced' and { [card.config.center.key] = true } or {} - end - if not SMODS.enh_cache:read(card, extra_only) then - - local enhancements = {} - if card.config.center.key ~= "c_base" then - enhancements[card.config.center.key] = true - end - local calc_return = {} - SMODS.calculate_context({other_card = card, check_enhancement = true, no_blueprint = true}, calc_return) - for _, eval in pairs(calc_return) do - for key, eval2 in pairs(eval) do - if type(eval2) == 'table' then - for key2, _ in pairs(eval2) do - if G.P_CENTERS[key2] then enhancements[key2] = true end - end - else - if G.P_CENTERS[key] then enhancements[key] = true end - end - end +function SMODS.shatters(card) + if not SMODS.set_quantum_cache(card) then return end + for key, q_field in pairs(SMODS.QuantumCardFields) do + for k, _ in pairs(SMODS.qfield_cache[card].get[q_field.return_flag]) do + local obj = q_field.g_obj_table[k] or {} + if obj.shatters then return true end end - SMODS.enh_cache:write(card, enhancements) end - return SMODS.enh_cache:read(card, extra_only) -end - -SMODS.enh_cache = { - write = function(self, key, value) - self.data[key] = value - end, - read = function(self, key, extra_only) - if not self.data[key] then return end - local ret = copy_table(self.data[key]) - if extra_only then ret[key.config.center.key] = nil end - return ret - end, - clear = function(self) - self.data = setmetatable({}, { __mode = 'k' }) - end, -} -SMODS.enh_cache:clear() - -function SMODS.has_enhancement(card, key) - if card.config.center.key == key then return true end - local enhancements = SMODS.get_enhancements(card) - if enhancements[key] then return true end return false end -function SMODS.shatters(card) - local enhancements = SMODS.get_enhancements(card) - for key, _ in pairs(enhancements) do - if G.P_CENTERS[key].shatters or key == 'm_glass' then return true end - end -end - function SMODS.get_ability_reset_keys(card) - local reset_keys = {'name', 'effect', 'set', 'extra', 'played_this_ante', 'perma_debuff'} + local reset_keys = {'name', 'effect', 'set', 'extra', 'played_this_ante', 'perma_debuff', 'Xmult'} for _, mod in ipairs(SMODS.mod_list) do if mod.set_ability_reset_keys then local keys = mod.set_ability_reset_keys() @@ -1094,79 +1019,25 @@ function SMODS.get_ability_reset_keys(card) end return reset_keys end - -function SMODS.calculate_quantum_enhancements(card, effects, context) - if not SMODS.optional_features.quantum_enhancements then return end - if context.extra_enhancement or context.check_enhancement or SMODS.extra_enhancement_calc_in_progress then return end - context.extra_enhancement = true - SMODS.extra_enhancement_calc_in_progress = true - local extra_enhancements = SMODS.get_enhancements(card, true) - local old_ability = copy_table(card.ability) - local old_center = card.config.center - local old_center_key = card.config.center_key - -- Note: For now, just trigger extra enhancements in order. - -- Future work: combine enhancements during - -- playing card scoring (ex. Mult comes before Glass because +_mult - -- naturally comes before x_mult) - local extra_enhancements_list = {} - for k, _ in pairs(extra_enhancements) do - if G.P_CENTERS[k] then - table.insert(extra_enhancements_list, k) - end - end - table.sort(extra_enhancements_list, function(a, b) return G.P_CENTERS[a].order < G.P_CENTERS[b].order end) - - for _, k in ipairs(extra_enhancements_list) do - card:quantum_set_ability(G.P_CENTERS[k]) - card.ability.extra_enhancement = k - local eval = eval_card(card, context) - table.insert(effects, eval) - end - card.ability = old_ability - card.config.center = old_center - card.config.center_key = old_center_key - context.extra_enhancement = nil - SMODS.extra_enhancement_calc_in_progress = nil -end - -function SMODS.has_no_suit(card) - local is_stone = false - local is_wild = false - for k, _ in pairs(SMODS.get_enhancements(card)) do - if k == 'm_stone' or G.P_CENTERS[k].no_suit then is_stone = true end - if k == 'm_wild' or G.P_CENTERS[k].any_suit then is_wild = true end - end - return is_stone and not is_wild -end -function SMODS.has_any_suit(card) - for k, _ in pairs(SMODS.get_enhancements(card)) do - if k == 'm_wild' or G.P_CENTERS[k].any_suit then return true end - end -end -function SMODS.has_no_rank(card) - for k, _ in pairs(SMODS.get_enhancements(card)) do - if k == 'm_stone' or G.P_CENTERS[k].no_rank then return true end - end -end function SMODS.always_scores(card) - for k, _ in pairs(SMODS.get_enhancements(card)) do - if k == 'm_stone' or G.P_CENTERS[k].always_scores then return true end - end - if (G.P_CENTERS[(card.edition or {}).key] or {}).always_scores then return true end - if (G.P_SEALS[card.seal or {}] or {}).always_scores then return true end - for k, v in pairs(SMODS.Stickers) do - if v.always_scores and card.ability[k] then return true end + if not SMODS.set_quantum_cache(card) then return end + for _, q_field in pairs(SMODS.QuantumCardFields) do + for k, _ in pairs(SMODS.qfield_cache[card].get[q_field.return_flag]) do + local obj = q_field.g_obj_table[k] or {} + if obj.always_scores then return true end + end end + return false end function SMODS.never_scores(card) - for k, _ in pairs(SMODS.get_enhancements(card)) do - if G.P_CENTERS[k].never_scores then return true end - end - if (G.P_CENTERS[(card.edition or {}).key] or {}).never_scores then return true end - if (G.P_SEALS[card.seal or {}] or {}).never_scores then return true end - for k, v in pairs(SMODS.Stickers) do - if v.never_scores and card.ability[k] then return true end + if not SMODS.set_quantum_cache(card) then return end + for _, q_field in pairs(SMODS.QuantumCardFields) do + for k, _ in pairs(SMODS.qfield_cache[card].get[q_field.return_flag]) do + local obj = q_field.g_obj_table[k] or {} + if obj.never_scores then return true end + end end + return false end SMODS.collection_pool = function(_base_pool) @@ -1297,6 +1168,23 @@ SMODS.smart_level_up_hand = function(card, hand, instant, amount, statustext) level_up_hand(card, hand, instant, (type(amount) == 'number' or type(amount) == 'table') and amount or 1, statustext) end + +SMODS.amount_return_flags = { + remove = true, + debuff_text = true, + cards_to_draw = true, + numerator = true, + denominator = true, + no_destroy = true, + replace_scoring_name = true, + replace_display_name = true, + replace_poker_hands = true, + no_mod = true, fixed = true, + modify = true, + shop_create_flags = true, + booster_create_flags = true +} + -- This function handles the calculation of each effect returned to evaluate play. -- Can easily be hooked to add more calculation effects ala Talisman SMODS.calculate_individual_effect = function(effect, scored_card, key, amount, from_edition) @@ -1464,22 +1352,6 @@ SMODS.calculate_individual_effect = function(effect, scored_card, key, amount, f return key end - local amount_return_flags = { - remove = true, - debuff_text = true, - cards_to_draw = true, - numerator = true, - denominator = true, - no_destroy = true, - replace_scoring_name = true, - replace_display_name = true, - replace_poker_hands = true, - modify = true, - override = true, - shop_create_flags = true, - booster_create_flags = true - } - if key == 'modify' then if SMODS.context_stack[#SMODS.context_stack].context.modify_final_cashout then if effect.cashout_row then @@ -1493,7 +1365,7 @@ SMODS.calculate_individual_effect = function(effect, scored_card, key, amount, f end end - if amount_return_flags[key] then + if SMODS.amount_return_flags[key] then return { [key] = amount } end @@ -1501,7 +1373,6 @@ SMODS.calculate_individual_effect = function(effect, scored_card, key, amount, f if key == 'debuff' then return { [key] = amount, debuff_source = scored_card } end - end -- Used to calculate a table of effects generated in evaluate_play @@ -1600,6 +1471,7 @@ SMODS.other_calculation_keys = { 'message', 'level_up', 'func', 'numerator', 'denominator', + 'fixed', 'modify', 'no_destroy', 'prevent_trigger', 'replace_scoring_name', 'replace_display_name', 'replace_poker_hands', @@ -1614,6 +1486,7 @@ SMODS.silent_calculation = { cards_to_draw = true, func = true, extra = true, numerator = true, denominator = true, + fixed = true, no_destroy = true, } @@ -1653,14 +1526,6 @@ SMODS.calculate_repetitions = function(card, context, reps) for _, value in pairs(eval) do SMODS.insert_repetitions(reps, value, card) end - -- Quantum enhancement support :cat_owl: - local quantum_eval = {} - SMODS.calculate_quantum_enhancements(card, quantum_eval, context) - for _, eval in ipairs(quantum_eval) do - for _, value in pairs(eval) do - SMODS.insert_repetitions(reps, value, card) - end - end context.repetition_only = nil --From jokers for _, area in ipairs(SMODS.get_card_areas('jokers')) do @@ -1776,19 +1641,6 @@ SMODS.calculate_retriggers = function(card, context, _ret) return retriggers end -function Card:calculate_edition(context) - if self.edition then - local edition = G.P_CENTERS[self.edition.key] - if edition.calculate and type(edition.calculate) == 'function' then - local o = edition:calculate(self, context) - if o then - if not o.card then o.card = self end - return o - end - end - end -end - function SMODS.calculate_card_areas(_type, context, return_table, args) local flags = {} if _type == 'jokers' then @@ -1839,7 +1691,7 @@ function SMODS.calculate_card_areas(_type, context, return_table, args) return_table[#return_table+1] = v end else - local f = SMODS.trigger_effects(effects, _card) + local f = SMODS.trigger_effects(effects, _card) or {} for k,v in pairs(f) do flags[k] = v end SMODS.update_context_flags(context, flags) end @@ -1854,11 +1706,12 @@ function SMODS.calculate_card_areas(_type, context, return_table, args) for _,v in ipairs(context.scoring_hand) do scoring_map[v] = true end end for _, area in ipairs(SMODS.get_card_areas('playing_cards')) do + if not args or not args.has_area then context.cardarea = area end if area == G.play and not context.scoring_hand then - -- If context is for probability, eval_card() anyway - -- This allows Seals, etc. to affect Joker probabilities during individual scoring: - -- For example; A seal can double the probability of Blood Stone hitting for the playing card it is applied to. - if context.mod_probability or context.fix_probability then + -- If context is a getter context (quantum/probability), eval_card() anyway + -- This allows Seals/etc. to affect Joker probabilities / card quantum values during individual scoring: + -- For example; A seal can double the probability of Blood Stone hitting for the playing card it is applied to, or make the card count as a lucky card. + if SMODS.is_getter_context(context) then for _, card in ipairs(area.cards) do if SMODS.check_looping_context(card) then goto skip @@ -1866,14 +1719,12 @@ function SMODS.calculate_card_areas(_type, context, return_table, args) local effects = {eval_card(card, context)} local f = SMODS.trigger_effects(effects, card) for k,v in pairs(f) do flags[k] = v end - SMODS.update_context_flags(context, flags) ::skip:: end end goto continue end - if not args or not args.has_area then context.cardarea = area end for _, card in ipairs(area.cards) do if SMODS.check_looping_context(card) then goto skip @@ -1890,10 +1741,8 @@ function SMODS.calculate_card_areas(_type, context, return_table, args) --calculate the played card effects if return_table then return_table[#return_table+1] = eval_card(card, context) - SMODS.calculate_quantum_enhancements(card, return_table, context) else local effects = {eval_card(card, context)} - SMODS.calculate_quantum_enhancements(card, effects, context) local f = SMODS.trigger_effects(effects, card) for k,v in pairs(f) do flags[k] = v end SMODS.update_context_flags(context, flags) @@ -1971,20 +1820,84 @@ function SMODS.update_context_flags(context, flags) if flags.replace_display_name then context.display_name = flags.replace_display_name end if flags.replace_poker_hands then context.poker_hands = flags.replace_poker_hands end end + local no_mod_changed = false + if flags.no_mod ~= nil then + context.no_mod = flags.no_mod + no_mod_changed = true + end + if context._quantum_getter or context.card_has_check then + SMODS.update_context_flags_qfields(context, flags) + end end +-- Subfunction of the above, updates a [context] with QuantumCardFields related [flags]. +function SMODS.update_context_flags_qfields(context, flags) + for key, q_field in pairs(SMODS.QuantumCardFields) do + if SMODS.optional_features.quantum_fields[key] then + local has_no_flag = "no_" .. key + local has_any_flag = "any_" .. key + if context[q_field.get_context_flag] and flags[q_field.return_flag] then + if not context.no_mod or no_mod_changed then + if flags.fixed == q_field.return_flag or type(flags.fixed) == "table" and flags.fixed[q_field.return_flag] then + local new_get = {} + for k, v in pairs(flags[q_field.return_flag]) do + new_get[k] = (context[q_field.return_flag][k] ~= "BASE" or not v) and not not v or "BASE" -- If it's a "BASE" value and it's not removed, keep it as "BASE" -> Required for correct quantum card.ability access + end + context[q_field.return_flag] = new_get + else + for k, v in pairs(flags[q_field.return_flag]) do + if context[q_field.return_flag][k] ~= "BASE" or not v then + context[q_field.return_flag][k] = v + end + end + end + end + SMODS.qfield_cache[context.card].get[q_field.return_flag] = context[q_field.return_flag] + flags[q_field.return_flag] = nil + end + if flags[has_no_flag] ~= nil then + SMODS.qfield_cache[context.card].has[key].no = not not flags[has_no_flag] + end + if flags[has_any_flag] ~= nil then + SMODS.qfield_cache[context.card].has[key].any = not not flags[has_any_flag] + end + end + end +end + +-- Enum, used for/with the below SMODS.get_context_type() +SMODS.CONTEXT_TYPES = { + PROBABILITY = "probability", + POST_TRIGGER = "post_trigger", + QUANTUM_GETTER = "quantum_getter" +} + -- Used to avoid looping getter context calls. Example; -- Joker A: Doubles lucky card probabilities -- Joker B: 1 in 3 chance that a card counts as a lucky card -- Joker A calls SMODS.has_enhancement() during a probability context to check whether it should double the numerator -- Joker B calls SMODS.pseudorandom_probability() to check whether it should trigger --- A loop is caused (ignore the fact that Joker B would be the trigger_obj and not a playing card) (I'd write a Quantum Ranks example, If I had any!!) --- To avoid this; Check before evaluating any object, whether the current getter context type (if it's a getter context) has previously caused said object to create a getter context, +-- A loop is caused (ignore the fact that Joker B would be the trigger_obj and not a playing card) +-- To avoid this; Check before evaluating any object, whether the current context type has previously caused said object to create a context of a previous type, -- if yes, don't evaluate the object. +function SMODS.get_context_type(context) + if not context then return end + if context.mod_probability or context.fix_probability then return SMODS.CONTEXT_TYPES.PROBABILITY end + if context.post_trigger then return SMODS.CONTEXT_TYPES.POST_TRIGGER end + if context._quantum_getter then return SMODS.CONTEXT_TYPES.QUANTUM_GETTER end +end + +SMODS.GETTER_CONTEXT_TYPES = { + [SMODS.CONTEXT_TYPES.PROBABILITY] = true, + [SMODS.CONTEXT_TYPES.QUANTUM_GETTER] = true, +} + function SMODS.is_getter_context(context) - if context.mod_probability or context.fix_probability then return "probability" end - if context.check_enhancement then return "enhancement" end - return false + return SMODS.GETTER_CONTEXT_TYPES[SMODS.get_context_type(context)] +end + +function SMODS.is_loopable_context(context) + return SMODS.get_context_type(context) ~= SMODS.CONTEXT_TYPES.QUANTUM_GETTER end SMODS.CONTEXT_RETRIGGER_BLACKLIST = { @@ -2029,13 +1942,14 @@ end function SMODS.check_looping_context(eval_object) if #SMODS.context_stack < 2 then return false end - local getter_type = SMODS.is_getter_context(SMODS.context_stack[#SMODS.context_stack].context) - if not getter_type then return end + local context_type = SMODS.get_context_type(SMODS.context_stack[#SMODS.context_stack].context) + if not context_type then return false end for i, t in ipairs(SMODS.context_stack) do - local other_type = SMODS.is_getter_context(t.context) - local next_context = SMODS.context_stack[i+1] - -- If the current kind of getter context has caused the eval_object to incite a getter context before, dont evaluate the object again - if other_type == getter_type and next_context and SMODS.is_getter_context(next_context.context) and next_context.caller == eval_object then + local other_type = SMODS.get_context_type(t.context) + local next_context_t = SMODS.context_stack[i+1] or {} + -- If the current kind of context has caused the eval_object to incite another context before, dont evaluate the object again + if other_type == context_type and SMODS.is_loopable_context(next_context_t.context) and next_context_t.caller == eval_object then + sendWarnMessage(("SMODS.check_looping_context prevented loop; context type '%s', caller '%s'"):format(context_type, eval_object), "Utils") return true end end @@ -2157,7 +2071,6 @@ function SMODS.score_card(card, context) context.main_scoring = true local effects = { eval_card(card, context) } - SMODS.calculate_quantum_enhancements(card, effects, context) context.main_scoring = nil context.individual = true context.other_card = card @@ -2229,7 +2142,6 @@ function SMODS.calculate_end_of_round_effects(context) context.playing_card_end_of_round = true --calculate the hand effects local effects = {eval_card(card, context)} - SMODS.calculate_quantum_enhancements(card, effects, context) context.playing_card_end_of_round = nil context.individual = true @@ -2539,6 +2451,8 @@ local function insert(t, res) end SMODS.optional_features = { cardareas = {}, + quantum_fields = {}, + quantum_straight_min_return = true } SMODS.get_optional_features = function() for _,mod in ipairs(SMODS.mod_list) do @@ -2685,53 +2599,23 @@ function SMODS.smeared_check(card, suit) return false end - if ((card.base.suit == 'Hearts' or card.base.suit == 'Diamonds') and (suit == 'Hearts' or suit == 'Diamonds')) then - return true - elseif (card.base.suit == 'Spades' or card.base.suit == 'Clubs') and (suit == 'Spades' or suit == 'Clubs') then - return true + if (card.base.suit == 'Hearts' or card.base.suit == 'Diamonds') and (not suit or (suit == 'Hearts' or suit == 'Diamonds')) then + return not suit and {Hearts = true, Diamonds = true} or true -- Modified to return the suits -> useful in SMODS.QuantumCardField.suit.base_getter + elseif (card.base.suit == 'Spades' or card.base.suit == 'Clubs') and (not suit or (suit == 'Spades' or suit == 'Clubs')) then + return not suit and {Spades = true, Clubs = true} or true -- Modified to return the suits -> useful in SMODS.QuantumCardField.suit.base_getter end return false end -local function has_any_other_suit(count, suit) - for k, v in pairs(count) do - if k ~= suit then - if v > 0 then - return true - end - end - end - return false -end - -local function saw_double(count, suit) - if count[suit] > 0 and has_any_other_suit(count, suit) then return true else return false end -end - function SMODS.seeing_double_check(hand, suit) - local suit_tally = {} - for i = #SMODS.Suit.obj_buffer, 1, -1 do - suit_tally[SMODS.Suit.obj_buffer[i]] = 0 - end - for i = 1, #hand do - if not SMODS.has_any_suit(hand[i]) then - for k, v in pairs(suit_tally) do - if hand[i]:is_suit(k) then suit_tally[k] = suit_tally[k] + 1 end - end - end - end - for i = 1, #hand do - if SMODS.has_any_suit(hand[i]) then - if hand[i]:is_suit(suit) and suit_tally[suit] == 0 then - suit_tally[suit] = 1 - else - for k, v in pairs(suit_tally) do - if hand[i]:is_suit(k) and suit_tally[k] == 0 then suit_tally[k] = 1 end - end - end - end + if #hand < 2 then return false end + local suit_tally = SMODS.get_suit_tally(hand) + if (suit_tally[suit] or 0) < 1 then return false end + for v, tally in pairs(suit_tally) do + -- If any other suit has cards and there's more than two cards. (tally > 0 is implicit, as else v wouldn't be indexed at all) + if v ~= suit then return true end end - if saw_double(suit_tally, suit) then return true else return false end + return false end local function parse_tooltip_vars(str, separator) @@ -3084,6 +2968,28 @@ function SMODS.wrap_around_straight() return false end +function SMODS.get_straight_ranks(t, objectified) + t = t or SMODS.Ranks + local ret = {} + for k, rank in pairs(t) do + local key = k + if type(k) == "table" then + rank = k + key = k.key + end + if rank and next(rank.virtual.ranks) then + for _, v_rank in ipairs(rank.virtual.ranks) do + if SMODS.VirtualRanks[v_rank] then + ret[objectified and SMODS.VirtualRanks[v_rank] or v_rank] = objectified or {} + end + end + else + ret[objectified and SMODS.Ranks[key] or key] = objectified or {} + end + end + return ret +end + function SMODS.merge_effects(...) local t = {} for _, v in ipairs({...}) do @@ -3166,7 +3072,7 @@ function SMODS.is_eternal(card, trigger) end end end - if card.ability.eternal then ret = true end + if card:has_sticker("eternal", {bypass_debuff = true}) then ret = true end if card.config.center.eternal_compat == false and not ovr_compat then ret = false end return ret end @@ -3911,6 +3817,25 @@ function CardArea:handle_card_limit() end +function SMODS.to_map(t) + if type(t) ~= "table" then return {} end + local ret = {} + for _, v in ipairs(t) do + ret[v] = true + end + return ret +end + +function SMODS.get_keys(t) + if type(t) ~= "table" then return {} end + local ret = {} + for k, _ in pairs(t) do + ret[#ret + 1] = k + end + table.sort(ret, function (a, b) return a < b end) + return ret +end + function SMODS.get_atlas(atlas_key) return G.ASSET_ATLAS[atlas_key] or G.ANIMATION_ATLAS[atlas_key] -- atlas.atlas_table = STATE_ATLAS -> also stored in G.ANIMATION_ATLAS end @@ -4444,6 +4369,66 @@ function SMODS.create_unlock_text(center) return localize('k_'..string.lower(center and center.set or 'unknown')) end +-- Takes a key_string of this form 'subfield.subfield.subfield ...' OR {'string key', 3 (numeric key), ...} (list of arbitrary keys) +function table_get_subfield(_table, key_string_or_keys) + if type(_table) ~= "table" then sendWarnMessage("table_get_subfield called with invalid table argument", "utils"); return end + if type(key_string_or_keys) ~= "string" and type(key_string_or_keys) ~= "table" then sendWarnMessage(string.format("table_get_subfield called with invalid key_string '%s'.", key_string_or_keys), "utils"); return end + local _t = _table + if type(key_string_or_keys) == "string" then + for field in string.gmatch(key_string_or_keys, "[^.]+") do + _t = _t[field] + if not _t then return end + end + else + for _, field in ipairs(key_string_or_keys) do + _t = _t[field] + if not _t then return end + end + end + return _t +end + +-- Recursive bipartite matching function +-- obj is one of the "jobs" (cards are "people") (lmao) +-- obj_to_cards is a map of "jobs" to all its "people" candidates +-- card_to_obj is a map of "people" to their current assigned "job" +-- used_c is a map of cards that have been assigned a "job" +function SMODS.recursive_bipartite_matching(obj, obj_to_cards, card_to_obj, used_c) + for pcard, _ in pairs(obj_to_cards[obj]) do + if not used_c[pcard] then + used_c[pcard] = true + if not card_to_obj[pcard] or SMODS.recursive_bipartite_matching(card_to_obj[pcard], obj_to_cards, card_to_obj, used_c) then + card_to_obj[pcard] = obj + return true + end + end + end +end + +-- Returns the number of "people" that can get a "job" (-> cards that can be assigned to a unique object), and the map of "person" to "job" (-> card to obj) +function SMODS.count_bipartite_matching(obj_to_cards) + local used_cards = {} + local card_to_obj = {} + local count = 0 + for obj, _ in pairs(obj_to_cards) do + used_cards = {} + if SMODS.recursive_bipartite_matching(obj, obj_to_cards, card_to_obj, used_cards) then + count = count + 1 + end + end + return count, card_to_obj +end + +-- Returns a list of card sorted by their position in their cardarea +function SMODS.get_sorted_card_list(card_map) + local ret = {} + for pcard, v in pairs(card_map) do + ret[#ret+1] = pcard + end + table.sort(ret, function (a, b) return a.rank < b.rank end) + return ret +end + function SMODS.copy_card(card, args) args = args or {} local playing_card