From b5293f95dc97f170041c32010b3810dbeb5352f1 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Sat, 4 Apr 2026 12:13:40 +0200 Subject: [PATCH 01/39] Moved over `SMODS.GameState` API from #1055. *Title --- lovely/game_state.toml | 31 +++ src/game_object.lua | 482 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 513 insertions(+) create mode 100644 lovely/game_state.toml diff --git a/lovely/game_state.toml b/lovely/game_state.toml new file mode 100644 index 000000000..4e667627f --- /dev/null +++ b/lovely/game_state.toml @@ -0,0 +1,31 @@ +[manifest] +version = "1.0.0" +dump_lua = true +priority = -10 + + +# game.lua : Game:update() : inject SMODS.GameStates +[[patches]] +[patches.pattern] +target = 'game.lua' +pattern = ''' + if self.STATE == self.STATES.MENU then + self:update_menu(dt) + end +''' +position = 'after' +match_indent = true +payload = ''' +if SMODS.GameStates[self.STATE] then + SMODS.GameStates[self.STATE]:update(dt) +end +''' + +# functions/button_callbacks.lua : G.FUNCS.use_card() : update logic +[[patches]] +[patches.pattern] +target = 'functions/button_callbacks.lua' +pattern = '''if card.ability.set == 'Booster' and not nosave and G.STATE == G.STATES.SHOP then''' +position = 'at' +match_indent = true +payload = '''if card.ability.set == 'Booster' and not nosave and G.STATE == SMODS.STATES.SHOP then''' \ No newline at end of file diff --git a/src/game_object.lua b/src/game_object.lua index 43b47f74f..fb0ccd897 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -3982,6 +3982,488 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. text = '^' } + ------------------------------------------------------------------------------------------------- + ----- API CODE SMODS.GameState + ------------------------------------------------------------------------------------------------- + + SMODS.STATES = { + BOOSTER_OPENED = "BOOSTER_OPENED", + REDEEM_VOUCHER = "REDEEM_VOUCHER", + SHOP = "SHOP", + ROUND_EVAL = "ROUND_EVAL", + BLIND = "BLIND", + BLIND_SELECT = "BLIND_SELECT" + } + SMODS.default_state = SMODS.STATES.BLIND_SELECT + + SMODS.state_stack = {} + + function SMODS.get_current_state() + return #SMODS.state_stack > 0 and SMODS.state_stack[#SMODS.state_stack] + end + + function SMODS.push_to_state_stack(state, args) + table.insert(SMODS.state_stack, {state=state, args=args}) + end + + function SMODS.pop_from_state_stack(state) + if #SMODS.state_stack < 1 then return end + if SMODS.state_stack[#SMODS.state_stack].state == state then + table.remove(SMODS.state_stack, #SMODS.state_stack) + end + end + + function SMODS.clear_states(exempt_map) + exempt_map = exempt_map or {} + if G.blind_select and not exempt_map[SMODS.STATES.BLIND_SELECT] then G.blind_select:remove(); G.blind_select = nil end + if G.shop and not exempt_map[SMODS.STATES.SHOP] then G.shop:remove(); G.shop = nil end + if G.buttons and not exempt_map[SMODS.STATES.BLIND] then G.buttons:remove(); G.buttons = nil end + if G.round_eval and not exempt_map[SMODS.STATES.ROUND_EVAL] then G.round_eval:remove(); G.round_eval = nil end + end + + function SMODS.enter_state(state, args, hold_state) + if G.STATE == state then return end + if SMODS.GameStates[G.STATE] and hold_state then + SMODS.GameStates[G.STATE]:on_exit({new_state=state}, true) + end + if not hold_state then + SMODS.pop_from_state_stack(G.STATE) + end + G.STATE = state + if SMODS.GameStates[G.STATE] then + SMODS.GameStates[G.STATE]:on_enter(args) + end + SMODS.push_to_state_stack(state, args) + end + + function SMODS.exit_state(args) + if SMODS.GameStates[G.STATE] then + SMODS.GameStates[G.STATE]:on_exit(args) + end + SMODS.pop_from_state_stack(G.STATE) + if #SMODS.context_stack < 1 then + G.STATE = nil + SMODS.enter_state(SMODS.default_state) + return + end + G.STATE = SMODS.context_stack[#SMODS.context_stack].state + if SMODS.GameStates[G.STATE] then + SMODS.GameStates[G.STATE]:on_enter(args, true) + end + end + + SMODS.GameStates = {} + SMODS.GameState = SMODS.GameObject:extend{ + set = 'GameState', + obj_table = SMODS.GameStates, + obj_buffer = {}, + required_parameters = { + 'key', + }, + on_enter = function (self, args, from_hold) end, + on_exit = function (self, args, from_hold) end, + update = function (self, dt) end, + ease_background_colour = nil, -- function + } + + SMODS.GameState { + key = SMODS.STATES.BOOSTER_OPENED, + update = function (self, dt) + SMODS.OPENED_BOOSTER.config.center:update_pack(dt) + end + } + + SMODS.GameState { + key = SMODS.STATES.REDEEM_VOUCHER + } + + SMODS.GameState { + key = SMODS.STATES.SHOP, + on_enter = function (self, args, from_hold) + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + func = function () + SMODS.clear_states({[SMODS.STATES.SHOP] = true}) + stop_use() + G.STATE_COMPLETE = true + ease_background_colour_blind(G.STATES.SHOP) + local shop_exists = not not G.shop + G.shop = G.shop or UIBox{ + definition = G.UIDEF.shop(), + config = {align='tmi', offset = {x=0,y=G.ROOM.T.y+11},major = G.hand, bond = 'Weak'} + } + -- Moved here from G.FUNCS.cash_out() + G.GAME.current_round.jokers_purchased = 0 + G.GAME.shop_free = nil + G.GAME.shop_d6ed = nil + ------- + G.E_MANAGER:add_event(Event({ + func = function() + G.shop.alignment.offset.y = -5.3 + G.shop.alignment.offset.x = 0 + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 0.2, + blockable = false, + func = function() + if math.abs(G.shop.T.y - G.shop.VT.y) < 3 then + G.ROOM.jiggle = G.ROOM.jiggle + 3 + play_sound('cardFan2') + for i = 1, #G.GAME.tags do + G.GAME.tags[i]:apply_to_run({type = 'shop_start'}) + end + local nosave_shop = nil + if not shop_exists then + if G.load_shop_jokers then + nosave_shop = true + G.shop_jokers:load(G.load_shop_jokers) + for k, v in ipairs(G.shop_jokers.cards) do + create_shop_card_ui(v) + if v.ability.consumeable then v:start_materialize() end + for _kk, vvv in ipairs(G.GAME.tags) do + if vvv:apply_to_run({type = 'store_joker_modify', card = v}) then break end + end + end + G.load_shop_jokers = nil + else + for i = 1, G.GAME.shop.joker_max - #G.shop_jokers.cards do + G.shop_jokers:emplace(create_card_for_shop(G.shop_jokers)) + end + end + + if G.load_shop_vouchers then + nosave_shop = true + G.shop_vouchers:load(G.load_shop_vouchers) + for k, v in ipairs(G.shop_vouchers.cards) do + create_shop_card_ui(v) + v:start_materialize() + end + G.load_shop_vouchers = nil + else + local vouchers_to_spawn = 0 + for _,_ in pairs(G.GAME.current_round.voucher.spawn) do vouchers_to_spawn = vouchers_to_spawn + 1 end + if vouchers_to_spawn < G.GAME.starting_params.vouchers_in_shop + (G.GAME.modifiers.extra_vouchers or 0) then + SMODS.get_next_vouchers(G.GAME.current_round.voucher) + end + for _, key in ipairs(G.GAME.current_round.voucher or {}) do + if G.P_CENTERS[key] and G.GAME.current_round.voucher.spawn[key] then + SMODS.add_voucher_to_shop(key) + end + end + end + + if G.load_shop_booster then + nosave_shop = true + G.shop_booster:load(G.load_shop_booster) + for k, v in ipairs(G.shop_booster.cards) do + create_shop_card_ui(v) + v:start_materialize() + end + G.load_shop_booster = nil + else + for i=1, G.GAME.starting_params.boosters_in_shop + (G.GAME.modifiers.extra_boosters or 0) do + G.GAME.current_round.used_packs = G.GAME.current_round.used_packs or {} + if not G.GAME.current_round.used_packs[i] then + G.GAME.current_round.used_packs[i] = get_pack('shop_pack').key + end + + if G.GAME.current_round.used_packs[i] ~= 'USED' then + local card = Card(G.shop_booster.T.x + G.shop_booster.T.w/2, + G.shop_booster.T.y, G.CARD_W*1.27, G.CARD_H*1.27, G.P_CARDS.empty, G.P_CENTERS[G.GAME.current_round.used_packs[i]], {bypass_discovery_center = true, bypass_discovery_ui = true}) + create_shop_card_ui(card, 'Booster', G.shop_booster) + card.ability.booster_pos = i + card:start_materialize() + G.shop_booster:emplace(card) + end + end + + for i = 1, #G.GAME.tags do + G.GAME.tags[i]:apply_to_run({type = 'voucher_add'}) + end + for i = 1, #G.GAME.tags do + G.GAME.tags[i]:apply_to_run({type = 'shop_final_pass'}) + end + end + end + + if not nosave_shop then SMODS.calculate_context({starting_shop = true}) end + G.CONTROLLER:snap_to({node = G.shop:get_UIE_by_ID('next_round_button')}) + if not nosave_shop then G.E_MANAGER:add_event(Event({ func = function() save_run(); return true end})) end + return true + end + end + })) + return true + end + })) + return true + end + })) + end, + on_exit = function (self, args, from_hold) + stop_use() + G.CONTROLLER.locks.toggle_shop = true + if G.shop then + if not from_hold then + SMODS.calculate_context({ending_shop = true}) + end + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + G.shop.alignment.offset.y = G.ROOM.T.y + 29 + G.SHOP_SIGN.alignment.offset.y = -15 + return true + end + })) + if from_hold then + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 0.5, + func = function () + G.CONTROLLER.locks.toggle_shop = nil + return true + end + })) + return + end + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 0.5, + func = function() + G.shop:remove() + G.shop = nil + G.SHOP_SIGN:remove() + G.SHOP_SIGN = nil + G.STATE_COMPLETE = false + G.CONTROLLER.locks.toggle_shop = nil + return true + end + })) + end + end, + check_win = true, + } + + SMODS.GameState { + key = SMODS.STATES.ROUND_EVAL, + on_enter = function (self, args, from_hold) + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + func = function () + SMODS.clear_states() + stop_use() + G.STATE_COMPLETE = true + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + save_run() + ease_background_colour_blind(G.STATES.ROUND_EVAL) + G.round_eval = UIBox{ + definition = create_UIBox_round_evaluation(), + config = {align="bm", offset = {x=0,y=G.ROOM.T.y + 19},major = G.hand, bond = 'Weak'} + } + G.round_eval.alignment.offset.x = 0 + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + if G.round_eval.alignment.offset.y ~= -7.8 then + G.round_eval.alignment.offset.y = -7.8 + else + if math.abs(G.round_eval.T.y - G.round_eval.VT.y) < 3 then + G.ROOM.jiggle = G.ROOM.jiggle + 3 + play_sound('cardFan2') + delay(0.1) + G.FUNCS.evaluate_round() + return true + end + end + end})) + return true + end + })) + return true + end + })) + end, + on_exit = function (self, args, from_hold) + stop_use() + if G.round_eval then + G.round_eval.alignment.offset.y = G.ROOM.T.y + 15 + G.round_eval.alignment.offset.x = 0 + G.deck:shuffle('cashout'..G.GAME.round_resets.ante) + G.deck:hard_set_T() + delay(0.3) + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + if G.round_eval then + G.round_eval:remove() + G.round_eval = nil + end + -- G.STATE_COMPLETE = false + return true + end + })) + ease_dollars(G.GAME.current_round.dollars) + G.E_MANAGER:add_event(Event({ + func = function() + G.GAME.previous_round.dollars = G.GAME.dollars + return true + end + })) + play_sound("coin7") + G.VIBRATION = G.VIBRATION + 1 + end + ease_chips(0) + reset_blinds() + delay(0.6) + end, + check_win = true, + } + + SMODS.GameState { + key = SMODS.STATES.BLIND, + on_enter = function (self, args, from_hold) + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + func = function () + SMODS.clear_states() + stop_use() + G.GAME.facing_blind = true + + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + ease_round(1) + inc_career_stat('c_rounds', 1) + if _DEMO then + G.SETTINGS.DEMO_ROUNDS = (G.SETTINGS.DEMO_ROUNDS or 0) + 1 + inc_steam_stat('demo_rounds') + G:save_settings() + end + G.GAME.round_resets.blind = G.P_BLINDS[args.key] + G.GAME.round_resets.blind_states[G.GAME.blind_on_deck] = 'Current' -- TODO : Check this / G.GAME.blind_on_deck + delay(0.2) + return true + end})) + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + new_round() + return true + end + })) + return true + end + })) + end, + on_exit = function (self, args, from_hold) + G.GAME.facing_blind = nil + if not from_hold then + -- Taken from G.FUNC.evaluate_round(), defeats blind + -- The extra nested immediate event should hopefully preserve the vanilla timing + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + func = function () + G.E_MANAGER:add_event(Event({ + trigger = 'before', + delay = 1.3*math.min(G.GAME.blind.dollars+2, 7)/2*0.15 + 0.5, + func = function() + G.GAME.blind:defeat() + G.GAME.current_round.discards_left = math.max(0, G.GAME.round_resets.discards + G.GAME.round_bonus.discards) + G.GAME.current_round.hands_left = (math.max(1, G.GAME.round_resets.hands + G.GAME.round_bonus.next_hands)) + return true + end + })) + return true + end + })) + ------ + end + + end, + ease_background_colour = function (self, blind_override) + local blindname = ((blind_override or (G.GAME.blind and G.GAME.blind.name ~= '' and G.GAME.blind.name)) or 'Small Blind') + blindname = (blindname == '' and 'Small Blind' or blindname) + + local boss_col = G.C.BLACK + for k, v in pairs(G.P_BLINDS) do + if v.name == blindname then + if v.boss and v.boss.showdown or v.blind_types and v.blind_types.Showdown then + ease_background_colour{new_colour = G.C.BLUE, special_colour = G.C.RED, tertiary_colour = darken(G.C.BLACK, 0.4), contrast = 3} + return + end + boss_col = v.boss_colour or G.C.BLACK + end + end + ease_background_colour{new_colour = lighten(mix_colours(boss_col, G.C.BLACK, 0.3), 0.1), special_colour = boss_col, contrast = 2} + end + } + + SMODS.GameState { + key = SMODS.STATES.BLIND_SELECT, + on_enter = function (self, args, from_hold) + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + func = function() + SMODS.clear_states() + stop_use() + ease_background_colour_blind(SMODS.STATES.BLIND_SELECT) + G.E_MANAGER:add_event(Event({ func = function() save_run(); return true end})) + G.CONTROLLER.interrupt.focus = true + G.E_MANAGER:add_event(Event({ func = function() + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + play_sound('cancel') + G.blind_select = SMODS.get_ante_path():create_ui() + G.blind_select.alignment.offset.y = 0.8-(G.hand.T.y - G.jokers.T.y) + G.blind_select.T.h + G.ROOM.jiggle = G.ROOM.jiggle + 3 + G.blind_select.alignment.offset.x = 0 + G.CONTROLLER.lock_input = false + for i = 1, #G.GAME.tags do + G.GAME.tags[i]:apply_to_run({type = 'immediate'}) + end + for i = 1, #G.GAME.tags do + if G.GAME.tags[i]:apply_to_run({type = 'new_blind_choice'}) then break end + end + return true + end + })) + return true + end})) + return true + end + })) + end, + on_exit = function (self, args, from_hold) + -- TODO : Figure out what this was doing + G.blind_prompt_box:get_UIE_by_ID('prompt_dynatext1').config.object.pop_delay = 0 + G.blind_prompt_box:get_UIE_by_ID('prompt_dynatext1').config.object:pop_out(5) + G.blind_prompt_box:get_UIE_by_ID('prompt_dynatext2').config.object.pop_delay = 0 + G.blind_prompt_box:get_UIE_by_ID('prompt_dynatext2').config.object:pop_out(5) + + G.E_MANAGER:add_event(Event({ + trigger = 'before', delay = 0.2, + func = function() + G.blind_prompt_box.alignment.offset.y = -10 + G.blind_select.alignment.offset.y = 40 + G.blind_select.alignment.offset.x = 0 + return true + end})) + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + func = function () + G.blind_select:remove() + G.blind_prompt_box:remove() + G.blind_select = nil + return true + end + })) + + end, + check_win = true, + } ------------------------------------------------------------------------------------------------- ----- API IMPORT GameObject.DrawStep From 13a051f57716392829f9844bbcff8ed11bc1034f Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Sat, 4 Apr 2026 12:37:28 +0200 Subject: [PATCH 02/39] More WIP moving. *Title --- lovely/booster.toml | 2 +- lovely/fixes.toml | 9 --- lovely/game_state.toml | 2 +- lovely/mod.toml | 15 ---- lovely/poker_hand.toml | 12 ---- src/overrides.lua | 157 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 159 insertions(+), 38 deletions(-) diff --git a/lovely/booster.toml b/lovely/booster.toml index 242a8abae..e4f2d8b1c 100644 --- a/lovely/booster.toml +++ b/lovely/booster.toml @@ -33,8 +33,8 @@ position = "at" payload = """ booster_obj = self.config.center if booster_obj and SMODS.Centers[booster_obj.key] then - G.STATE = G.STATES.SMODS_BOOSTER_OPENED SMODS.OPENED_BOOSTER = self + SMODS.enter_state(SMODS.STATES.BOOSTER_OPENED) end G.GAME.pack_choices = math.min((self.ability.choose or self.config.center.config.choose or 1) + (G.GAME.modifiers.booster_choice_mod or 0), self.ability.extra and math.max(1, self.ability.extra + (G.GAME.modifiers.booster_size_mod or 0)) or self.config.center.extra and math.max(1, self.config.center.extra + (G.GAME.modifiers.booster_size_mod or 0)) or 1) """ diff --git a/lovely/fixes.toml b/lovely/fixes.toml index e1ee49131..1aa3ed8d4 100644 --- a/lovely/fixes.toml +++ b/lovely/fixes.toml @@ -422,15 +422,6 @@ 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" -# end_round() -[[patches]] -[patches.pattern] -target = "functions/state_events.lua" -pattern = "local game_over = true" -position = "before" -payload = "G.GAME.blind.in_blind = false" -match_indent = true - # Allow winning game if winning ante is skipped [[patches]] [patches.pattern] diff --git a/lovely/game_state.toml b/lovely/game_state.toml index 4e667627f..48e11e2c5 100644 --- a/lovely/game_state.toml +++ b/lovely/game_state.toml @@ -28,4 +28,4 @@ target = 'functions/button_callbacks.lua' pattern = '''if card.ability.set == 'Booster' and not nosave and G.STATE == G.STATES.SHOP then''' position = 'at' match_indent = true -payload = '''if card.ability.set == 'Booster' and not nosave and G.STATE == SMODS.STATES.SHOP then''' \ No newline at end of file +payload = '''if card.ability.set == 'Booster' and not nosave and G.STATE == SMODS.STATES.SHOP then''' diff --git a/lovely/mod.toml b/lovely/mod.toml index 8feb0d80f..cd4d5deda 100644 --- a/lovely/mod.toml +++ b/lovely/mod.toml @@ -5,21 +5,6 @@ priority = -5 ### Per-mod functions -# end_round() -[[patches]] -[patches.regex] -target = 'functions/state_events.lua' -pattern = '''(?[\t ]*)reset_castle_card\(\)''' -line_prepend = '$indent' -position = 'after' -payload = ''' - -for _, mod in ipairs(SMODS.mod_list) do - if mod.reset_game_globals and type(mod.reset_game_globals) == 'function' then - mod.reset_game_globals(false) - end -end''' - # Game:start_run() [[patches]] [patches.regex] diff --git a/lovely/poker_hand.toml b/lovely/poker_hand.toml index 8bb29183a..5ec2f7ed1 100644 --- a/lovely/poker_hand.toml +++ b/lovely/poker_hand.toml @@ -112,16 +112,4 @@ pattern = '''G.GAME.hands[text].played_this_round = G.GAME.hands[text].played_th position = "after" payload = '''G.GAME.hands[text].played_this_ante = G.GAME.hands[text].played_this_ante + 1''' match_indent = true -times = 1 - -# end_round() -[[patches]] -[patches.pattern] -target = "functions/state_events.lua" -pattern = '''G.GAME.voucher_restock = nil''' -position = "after" -payload = '''for k, v in pairs(G.GAME.hands) do - v.played_this_ante = 0 -end''' -match_indent = true times = 1 \ No newline at end of file diff --git a/src/overrides.lua b/src/overrides.lua index ab6eb271a..f1cbc4d64 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -2718,4 +2718,161 @@ G.FUNCS.change_viewed_back = function(...) card.original_T = copy_table(card.T) end return g_funcs_change_viewed_back_ref(...) +end + + +-- SMODS.GameState related overrides +local ease_bg_col_bl_ref = ease_background_colour_blind +function ease_background_colour_blind(state, blind_override) + if SMODS.GameStates[state] and SMODS.GameStates[state].ease_background_colour then + return SMODS.GameStates[state]:ease_background_colour(blind_override) + end + return ease_bg_col_bl_ref(state, blind_override) +end + +function end_round() + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 0.2, + func = function() + G.GAME.blind.in_blind = false + local game_over = true + local game_won = false + G.RESET_BLIND_STATES = true + G.RESET_JIGGLES = true + if G.GAME.chips - G.GAME.blind.chips >= 0 then + game_over = false + end + -- context.end_of_round calculations + SMODS.saved = false + G.GAME.saved_text = nil + SMODS.calculate_context({end_of_round = true, game_over = game_over, beat_boss = G.GAME.blind.boss }) + if SMODS.saved then game_over = false end + -- TARGET: main end_of_round evaluation + if G.GAME.round_resets.ante == G.GAME.win_ante and G.GAME.blind:is_type("Boss") then + game_won = true + G.GAME.won = true + end + if game_over then + G.STATE = G.STATES.GAME_OVER + if not G.GAME.won and not G.GAME.seeded and not G.GAME.challenge then + G.PROFILES[G.SETTINGS.profile].high_scores.current_streak.amt = 0 + end + G:save_settings() + G.FILE_HANDLER.force = true + G.STATE_COMPLETE = false + else + G.GAME.unused_discards = (G.GAME.unused_discards or 0) + G.GAME.current_round.discards_left + if G.GAME.blind and G.GAME.blind.config.blind then + discover_card(G.GAME.blind.config.blind) + end + + if G.GAME.blind:is_type("Boss") then + local _handname, _played, _order = 'High Card', -1, 100 + for k, v in pairs(G.GAME.hands) do + if v.played > _played or (v.played == _played and _order > v.order) then + _played = v.played + _handname = k + end + end + G.GAME.current_round.most_played_poker_hand = _handname + end + + if G.GAME.blind:is_type("Boss") and not G.GAME.seeded and not G.GAME.challenge then + G.GAME.current_boss_streak = G.GAME.current_boss_streak + 1 + check_and_set_high_score('boss_streak', G.GAME.current_boss_streak) + end + + if G.GAME.current_round.hands_played == 1 then + inc_career_stat('c_single_hand_round_streak', 1) + else + if not G.GAME.seeded and not G.GAME.challenge then + G.PROFILES[G.SETTINGS.profile].career_stats.c_single_hand_round_streak = 0 + G:save_settings() + end + end + + check_for_unlock({type = 'round_win'}) + set_joker_usage() + if game_won and not G.GAME.win_notified then + G.GAME.win_notified = true + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + blocking = false, + blockable = false, + func = (function() + if SMODS.GameStates[G.STATE] and SMODS.GameStates[G.STATE].check_win then + win_game() + G.GAME.won = true + return true + end + end) + })) + end + for _,v in ipairs(SMODS.get_card_areas('playing_cards', 'end_of_round')) do + SMODS.calculate_end_of_round_effects({ cardarea = v, end_of_round = true, beat_boss = G.GAME.blind.boss }) + end + + G.FUNCS.draw_from_hand_to_discard() + if G.GAME.blind:is_type("Boss") then + G.GAME.voucher_restock = nil + for k, v in pairs(G.GAME.hands) do + v.played_this_ante = 0 + end + if G.GAME.modifiers.set_eternal_ante and (G.GAME.round_resets.ante == G.GAME.modifiers.set_eternal_ante) then + for k, v in ipairs(G.jokers.cards) do + v:set_eternal(true) + end + end + if G.GAME.modifiers.set_joker_slots_ante and (G.GAME.round_resets.ante == G.GAME.modifiers.set_joker_slots_ante) then + G.jokers.config.card_limit = 0 + end + delay(0.4) + end + G.FUNCS.draw_from_discard_to_deck() + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 0.3, + func = function() + if SMODS.get_current_state().args.trigger_callbacks then + SMODS.get_active_ap_node():trigger_callbacks("defeated") + end + G.STATE_COMPLETE = false + + if G.GAME.round_resets.blind == G.P_BLINDS.bl_small then + -- TODO : Check/Replace blind_states + G.GAME.round_resets.blind_states.Small = 'Defeated' + elseif G.GAME.round_resets.blind == G.P_BLINDS.bl_big then + G.GAME.round_resets.blind_states.Big = 'Defeated' + else + G.GAME.current_round.voucher = SMODS.get_next_vouchers() + G.GAME.round_resets.blind_states.Boss = 'Defeated' + for k, v in ipairs(G.playing_cards) do + v.ability.played_this_ante = nil + end + end + + if G.GAME.round_resets.temp_handsize then G.hand:change_size(-G.GAME.round_resets.temp_handsize); G.GAME.round_resets.temp_handsize = nil end + if G.GAME.round_resets.temp_reroll_cost then G.GAME.round_resets.temp_reroll_cost = nil; calculate_reroll_cost(true) end + + reset_idol_card() + reset_mail_rank() + reset_ancient_card() + reset_castle_card() + for _, mod in ipairs(SMODS.mod_list) do + if mod.reset_game_globals and type(mod.reset_game_globals) == 'function' then + mod.reset_game_globals(false) + end + end + for k, v in ipairs(G.playing_cards) do + v.ability.discarded = nil + v.ability.forced_selection = nil + end + return true + end + })) + end + return true + end + })) end \ No newline at end of file From 1a377663d552a01c12178d1191c740be7c35b82d Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Sat, 4 Apr 2026 12:43:21 +0200 Subject: [PATCH 03/39] Also moved over Blind types *Title --- src/overrides.lua | 130 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 129 insertions(+), 1 deletion(-) diff --git a/src/overrides.lua b/src/overrides.lua index f1cbc4d64..0f4c5e7a1 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -2875,4 +2875,132 @@ function end_round() return true end })) -end \ No newline at end of file +end + +--[[ +blind_states and loc_blind_states + +Game:update(dt) -> This; + if G.prev_small_state ~= G.GAME.round_resets.blind_states.Small or + G.prev_large_state ~= G.GAME.round_resets.blind_states.Big or + G.prev_boss_state ~= G.GAME.round_resets.blind_states.Boss or G.RESET_BLIND_STATES then ... +can probably be ignored and replaced with own system + +Game:start_run() -> get_next_tag_key() is called, besides that; blind_states is set +]] +function reset_blinds() + G.GAME.round_resets.boss_rerolled = false +end + +function SMODS.get_blind_types(blind_obj) + if type(blind_obj.get_types) == "function" then + return blind_obj:get_types() + else + if v.boss then + if not v.boss.showdown then + return {Boss = true} + else + return {Showdown = true} + end + else + if v.name == "Small Blind" then + return {Small = true} + else + return {Big = true} + end + end + end +end + +function get_new_boss() + sendWarnMessage("get_new_boss() is deprecated; Call SMODS.get_new_blind() instead.", "utils") +end + +function SMODS.get_new_blind(blind_types) + if not blind_types or (blind_types.Boss or blind_types.Showdown) then + G.GAME.perscribed_bosses = G.GAME.perscribed_bosses or {} + if G.GAME.perscribed_bosses and G.GAME.perscribed_bosses[G.GAME.round_resets.ante] then + local ret_boss = G.GAME.perscribed_bosses[G.GAME.round_resets.ante] + G.GAME.perscribed_bosses[G.GAME.round_resets.ante] = nil + G.GAME.bosses_used[ret_boss] = G.GAME.bosses_used[ret_boss] + 1 + return ret_boss + end + if G.FORCE_BOSS then return G.FORCE_BOSS end + end + + local eligible_bosses = {} + for k, v in pairs(G.P_BLINDS) do + local res = SMODS.add_to_pool(v) + if res then + local b_types = SMODS.get_blind_types(v) + for _, b_type in pairs(b_types) do + if blind_types[b_type] then + if v.in_pool or not v.boss or (v.boss.min <= math.max(1, G.GAME.round_resets.ante) and ((math.max(1, G.GAME.round_resets.ante))%G.GAME.win_ante ~= 0 or G.GAME.round_resets.ante < 2)) then + eligible_bosses[k] = true + break + end + end + end + end + end + for k, v in pairs(G.GAME.banned_keys) do + if eligible_bosses[k] then eligible_bosses[k] = nil end + end + + local min_use = 100 + for k, v in pairs(G.GAME.bosses_used) do + if eligible_bosses[k] then + eligible_bosses[k] = v + if eligible_bosses[k] <= min_use then + min_use = eligible_bosses[k] + end + end + end + for k, v in pairs(eligible_bosses) do + if eligible_bosses[k] then + if eligible_bosses[k] > min_use then + eligible_bosses[k] = nil + end + end + end + local _, boss = pseudorandom_element(eligible_bosses, pseudoseed('boss')) + G.GAME.bosses_used[boss] = G.GAME.bosses_used[boss] + 1 + + return boss +end + +function Blind:get_type() + return SMODS.get_blind_types(self.config.blind) +end + +function Blind:is_type(b_type) + return self:is_types({[b_type] = true}, false) +end + +function Blind:is_types(b_types_map, all) + for k, v in pairs(self:get_type()) do + if v and b_types_map[k] or (k == "Showdown" and b_types_map.Boss) then + if not all then + return true + end + elseif all then + return false + end + end + return all +end + +SMODS.Joker:take_ownership("j_matador", { + check_for_unlock = function (self, args) + return G.GAME.current_round.hands_played == 1 and G.GAME.current_round.discards_left == G.GAME.round_resets.discards and G.GAME.blind:is_type("Boss") + end +}) + +SMODS.Joker:take_ownership("j_hanging_chad", { + check_for_unlock = function (self, args) + return G.GAME.last_hand_played == self.unlock_condition.extra and G.GAME.blind:is_type("Boss") + end +}) + +-- Create Blind Select UI -> Not used in SMODS.STATES.BLIND_SELECT.on_enter() +function create_UIBox_blind_select() end \ No newline at end of file From 2beaffd97b6d12c7dcba232dcc40a6922b301003 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Sat, 4 Apr 2026 12:48:29 +0200 Subject: [PATCH 04/39] More moving. *Title --- lovely/blind.toml | 53 ++++++++++++++++++++++++++++++++++++++++++++- src/game_object.lua | 4 ++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/lovely/blind.toml b/lovely/blind.toml index 9d1771fa1..2ba89aa57 100644 --- a/lovely/blind.toml +++ b/lovely/blind.toml @@ -587,4 +587,55 @@ pattern = "local blindTable = {" position = "after" payload = ''' effect = self.effect, -''' \ No newline at end of file +''' + +### Account for changed Blind:get_type() function +# Blind.set_blind : Use new Blind:is_types() +[[patches]] +[patches.pattern] +target = "blind.lua" +match_indent = true +pattern = "if G.GAME.modifiers.no_blind_reward and G.GAME.modifiers.no_blind_reward[self:get_type()] then self.dollars = 0 end" +position = "at" +payload = '''if G.GAME.modifiers.no_blind_reward and self:is_types(G.GAME.modifiers.no_blind_reward) then self.dollars = 0 end''' +# game.lua : Game:start_run() : account for "Showdown" type with "no_reward" rule +[[patches]] +[patches.pattern] +target = "game.lua" +match_indent = true +pattern = "self.GAME.modifiers.no_blind_reward.Boss = true" +position = "after" +payload = '''self.GAME.modifiers.no_blind_reward.Showdown = true''' +# card.lua : Card:generate_UIBox_ability_table() +[[patches]] +[patches.pattern] +target = "game.lua" +match_indent = true +pattern = "local disableable = G.GAME.blind and ((not G.GAME.blind.disabled) and (G.GAME.blind:get_type() == 'Boss'))" +position = "at" +payload = '''local disableable = G.GAME.blind and ((not G.GAME.blind.disabled) and (G.GAME.blind:is_type("Boss")))''' +# card.lua : Card:calculate_joker() : Luchador !-- could be taken ownership of instead +[[patches]] +[patches.pattern] +target = "game.lua" +match_indent = true +pattern = "if G.GAME.blind and ((not G.GAME.blind.disabled) and (G.GAME.blind:get_type() == 'Boss')) then " +position = "at" +payload = '''if G.GAME.blind and ((not G.GAME.blind.disabled) and (G.GAME.blind:is_type("Boss"))) then ''' +# functions/button_callbacks.lua : G.FUNCS.HUD_blind_reward() +[[patches]] +[patches.pattern] +target = "functions/button_callbacks.lua" +match_indent = true +pattern = "if G.GAME.modifiers.no_blind_reward and (G.GAME.blind and G.GAME.modifiers.no_blind_reward[G.GAME.blind:get_type()]) then" +position = "at" +payload = ''' +if G.GAME.modifiers.no_blind_reward and (G.GAME.blind and blind:is_types(G.GAME.modifiers.no_blind_reward)) then +''' +# game.lua : Game:init_game_object() +[[patches]] +[patches.regex] +target = "game.lua" +pattern = '''if v.boss then bosses_used[k] = 0 end''' +position = "at" +payload = '''if SMODS.get_blind_types(blind_obj).Boss then bosses_used[k] = 0 end''' \ No newline at end of file diff --git a/src/game_object.lua b/src/game_object.lua index fb0ccd897..ac295ad05 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -1808,6 +1808,7 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. config = {}, dollars = 5, mult = 2, + blind_types = nil, -- Map of types, used by SMODS.get_new_blind() atlas = 'blind_chips', discovered = false, pos = { x = 0, y = 0 }, @@ -1827,6 +1828,9 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. end G.P_BLINDS[self.key] = self if self.modifies_draw then SMODS.Blinds.modifies_draw[self.key] = true end + end, + get_types = function(self) + return self.blind_types end } SMODS.Blind:take_ownership('eye', { From 42f50c2c57413b2471b846957cc411dde30d4552 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Sat, 4 Apr 2026 13:09:47 +0200 Subject: [PATCH 05/39] More *Title --- lovely/booster.toml | 54 +++++++++++------------------------------- lovely/fixes.toml | 2 +- lovely/game_state.toml | 33 ++++++++++++++++++++++++++ src/overrides.lua | 8 ++++++- src/utils.lua | 6 ++--- 5 files changed, 58 insertions(+), 45 deletions(-) diff --git a/lovely/booster.toml b/lovely/booster.toml index e4f2d8b1c..516bafeeb 100644 --- a/lovely/booster.toml +++ b/lovely/booster.toml @@ -89,32 +89,6 @@ payload = ''' SMODS.calculate_context({modify_booster_card = true, booster = self, card = card, index = i}) ''' -# Game:set_globals -[[patches]] -[patches.regex] -target = "globals.lua" -pattern = '''(?[\t ]*)self\.STATES = \{''' -position = "after" -payload = ''' - - SMODS_BOOSTER_OPENED = 999, - SMODS_REDEEM_VOUCHER = 998,''' -line_prepend = '$indent' - -# Game:update -[[patches]] -[patches.regex] -target = "game.lua" -pattern = '''(?[\t ]*)if self\.STATE == self\.STATES\.TAROT_PACK then''' -position = "before" -payload = ''' -if G.STATE == G.STATES.SMODS_BOOSTER_OPENED then - SMODS.OPENED_BOOSTER.config.center:update_pack(dt) -end - -''' -line_prepend = '$indent' - # G.FUNC.can_skip_booster # TODO customize whether pack can be skipped [[patches]] @@ -122,7 +96,7 @@ line_prepend = '$indent' target = "functions/button_callbacks.lua" pattern = '''(?[\t ]*)\(G\.STATE == G\.STATES\.PLANET_PACK or G\.STATE == G\.STATES\.STANDARD_PACK''' position = "at" -payload = '''(G.STATE == G.STATES.SMODS_BOOSTER_OPENED or G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.STANDARD_PACK''' +payload = '''(G.STATE == SMODS.STATES.BOOSTER_OPENED or G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.STANDARD_PACK''' # CardArea:draw() [[patches]] @@ -131,7 +105,7 @@ target = "cardarea.lua" pattern = "(self.config.type == 'deck' and self ~= G.deck) or" position = "before" payload = ''' -(self.config.type == 'hand' and state == G.STATES.SMODS_BOOSTER_OPENED) or''' +(self.config.type == 'hand' and state == SMODS.STATES.BOOSTER_OPENED) or''' match_indent = true # G.FUNCS.use_card @@ -141,7 +115,7 @@ target = "functions/button_callbacks.lua" pattern = "prev_state == G.STATES.SPECTRAL_PACK or prev_state == G.STATES.STANDARD_PACK or" position = "after" payload = ''' -prev_state == G.STATES.SMODS_BOOSTER_OPENED or''' +prev_state == SMODS.STATES.BOOSTER_OPENED or''' match_indent = true # CardArea:align_cards() @@ -150,7 +124,7 @@ match_indent = true target = "cardarea.lua" pattern = "if self.config.type == 'hand' and (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.PLANET_PACK) then" position = "at" -payload = "if self.config.type == 'hand' and (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.SMODS_BOOSTER_OPENED) then" +payload = "if self.config.type == 'hand' and (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED) then" match_indent = true # CardArea:align_cards() @@ -159,7 +133,7 @@ match_indent = true target = "cardarea.lua" pattern = "if self.config.type == 'hand' and not (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.PLANET_PACK) then" position = "at" -payload = "if self.config.type == 'hand' and not (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.SMODS_BOOSTER_OPENED) then" +payload = "if self.config.type == 'hand' and not (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED) then" match_indent = true # Card:can_use_consumable() @@ -168,7 +142,7 @@ match_indent = true target = "card.lua" pattern = "if G.STATE == G.STATES.SELECTING_HAND or G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.PLANET_PACK then" position = "at" -payload = "if G.STATE == G.STATES.SELECTING_HAND or G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.SMODS_BOOSTER_OPENED then" +payload = "if G.STATE == G.STATES.SELECTING_HAND or G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED then" match_indent = true # G.FUNCS.use_card() @@ -183,7 +157,7 @@ if nc then play_sound('cardSlide2', nil, 0.3) dont_dissolve = true end -if (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.SMODS_BOOSTER_OPENED) then""" +if (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED) then""" match_indent = true # G.FUNC.use_card() @@ -203,7 +177,7 @@ target = "functions/button_callbacks.lua" pattern = '''if prev_state == G.STATES.TAROT_PACK then inc_career_stat('c_tarot_reading_used', 1) end''' position = 'at' match_indent = true -payload = '''if prev_state == G.STATES.SMODS_BOOSTER_OPENED and booster_obj.name:find('Arcana') then inc_career_stat('c_tarot_reading_used', 1) end''' +payload = '''if prev_state == SMODS.STATES.BOOSTER_OPENED and booster_obj.name:find('Arcana') then inc_career_stat('c_tarot_reading_used', 1) end''' # G.FUNC.use_card() [[patches]] @@ -212,7 +186,7 @@ target = "functions/button_callbacks.lua" pattern = '''if prev_state == G.STATES.PLANET_PACK then inc_career_stat('c_planetarium_used', 1) end''' position = 'at' match_indent = true -payload = '''if prev_state == G.STATES.SMODS_BOOSTER_OPENED and booster_obj.name:find('Celestial') then inc_career_stat('c_planetarium_used', 1) end''' +payload = '''if prev_state == SMODS.STATES.BOOSTER_OPENED and booster_obj.name:find('Celestial') then inc_career_stat('c_planetarium_used', 1) end''' # G.FUNC.use_card() [[patches]] @@ -220,7 +194,7 @@ payload = '''if prev_state == G.STATES.SMODS_BOOSTER_OPENED and booster_obj.name target = "functions/button_callbacks.lua" pattern = "(G.STATE == G.STATES.BUFFOON_PACK and G.STATES.BUFFOON_PACK) or" position = "before" -payload = "(G.STATE == G.STATES.SMODS_BOOSTER_OPENED and G.STATES.SMODS_BOOSTER_OPENED) or" +payload = "(G.STATE == SMODS.STATES.BOOSTER_OPENED and SMODS.STATES.BOOSTER_OPENED) or" match_indent = true # G.FUNC.use_card() @@ -229,7 +203,7 @@ match_indent = true target = "functions/state_events.lua" pattern = "if not (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK) and" position = "at" -payload = "if not (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.SMODS_BOOSTER_OPENED) and" +payload = "if not (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED) and" match_indent = true # Card:use_consumeable() @@ -239,8 +213,8 @@ target = "card.lua" pattern = '''(?[\t ]*)align = \(G\.STATE[\s\S]*and -0\.2 or 0},''' position = "at" payload = ''' -align = (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.SMODS_BOOSTER_OPENED) and 'tm' or 'cm', -offset = {x = 0, y = (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.SMODS_BOOSTER_OPENED) and -0.2 or 0},''' +align = (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED) and 'tm' or 'cm', +offset = {x = 0, y = (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED) and -0.2 or 0},''' line_prepend = '$indent' # G.FUNCS.use_card() @@ -367,7 +341,7 @@ target = 'card.lua' match_indent = true position = 'after' pattern = '''if self.shop_voucher then G.GAME.current_round.voucher = nil end''' -payload = '''G.STATE = G.STATES.SMODS_REDEEM_VOUCHER''' +payload = '''G.STATE = SMODS.STATES.REDEEM_VOUCHER''' # G.FUNCS.use_card() # Consumables in areas other than the consumable don't count diff --git a/lovely/fixes.toml b/lovely/fixes.toml index 1aa3ed8d4..e2e21f067 100644 --- a/lovely/fixes.toml +++ b/lovely/fixes.toml @@ -78,7 +78,7 @@ target = 'functions/misc_functions.lua' pattern = "function save_run()" position = "after" payload = """ if G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.SPECTRAL_PACK - or G.STATE == G.STATES.BUFFOON_PACK or G.STATE == G.STATES.STANDARD_PACK or G.STATE == G.STATES.SMODS_BOOSTER_OPENED then return end""" + or G.STATE == G.STATES.BUFFOON_PACK or G.STATE == G.STATES.STANDARD_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED then return end""" match_indent = true ## Set `G.your_collection.config.collection` to true in all cases diff --git a/lovely/game_state.toml b/lovely/game_state.toml index 48e11e2c5..1952b72fd 100644 --- a/lovely/game_state.toml +++ b/lovely/game_state.toml @@ -21,6 +21,30 @@ if SMODS.GameStates[self.STATE] then end ''' +# Game:start_run() +[[patches]] +[patches.pattern] +target = 'game.lua' +match_indent = true +position = 'at' +pattern = ''' + self:prep_stage(G.STAGES.RUN, saveTable and saveTable.STATE or G.STATES.BLIND_SELECT) +''' +payload = ''' +self:prep_stage(G.STAGES.RUN, saveTable and saveTable.STATE or SMODS.STATES.BLIND_SELECT) +''' +[[patches]] +[patches.pattern] +target = 'game.lua' +match_indent = true +position = 'at' +pattern = ''' + self:prep_stage(G.STAGES.RUN, saveTable and saveTable.STATE or G.STATES.BLIND_SELECT) +''' +payload = ''' +self:prep_stage(G.STAGES.RUN, saveTable and saveTable.STATE or SMODS.STATES.BLIND_SELECT) +''' + # functions/button_callbacks.lua : G.FUNCS.use_card() : update logic [[patches]] [patches.pattern] @@ -29,3 +53,12 @@ pattern = '''if card.ability.set == 'Booster' and not nosave and G.STATE == G.ST position = 'at' match_indent = true payload = '''if card.ability.set == 'Booster' and not nosave and G.STATE == SMODS.STATES.SHOP then''' + +# functions/button_callbacks.lua : G.FUNCS.use_card() : update logic +[[patches]] +[patches.pattern] +target = 'functions/button_callbacks.lua' +pattern = '''(self.config.type == 'hand' and ({[G.STATES.SHOP]=1, [G.STATES.TAROT_PACK]=1, [G.STATES.SPECTRAL_PACK]=1, [G.STATES.STANDARD_PACK]=1,[G.STATES.BUFFOON_PACK]=1,[G.STATES.PLANET_PACK]=1, [G.STATES.ROUND_EVAL]=1, [G.STATES.BLIND_SELECT]=1})[state]) or''' +position = 'at' +match_indent = true +payload = '''(self.config.type == 'hand' and ({[SMODS.STATES.SHOP]=1, [G.STATES.TAROT_PACK]=1, [G.STATES.SPECTRAL_PACK]=1, [G.STATES.STANDARD_PACK]=1,[G.STATES.BUFFOON_PACK]=1,[G.STATES.PLANET_PACK]=1, [SMODS.STATES.ROUND_EVAL]=1, [SMODS.STATES.BLIND_SELECT]=1})[state]) or''' \ No newline at end of file diff --git a/src/overrides.lua b/src/overrides.lua index 0f4c5e7a1..ddc33fd10 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -2694,7 +2694,7 @@ local set_ability = Card.set_ability function Card:set_ability(center, initial, delay_sprites) local old_center = self.config.center 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 + if not initial and (G.STATE ~= SMODS.STATES.BOOSTER_OPENED and G.STATE ~= SMODS.STATES.SHOP and not G.SETTINGS.paused or G.TAROT_INTERRUPT) then SMODS.calculate_context({setting_ability = true, old = old_center.key, new = self.config.center_key, other_card = self, unchanged = old_center.key == self.config.center.key}) end self.front_hidden = self:should_hide_front() @@ -2721,7 +2721,13 @@ G.FUNCS.change_viewed_back = function(...) end + -- SMODS.GameState related overrides + +G.FUNCS.toggle_shop = function(e) + SMODS.enter_state(SMODS.STATES.BLIND_SELECT) +end + local ease_bg_col_bl_ref = ease_background_colour_blind function ease_background_colour_blind(state, blind_override) if SMODS.GameStates[state] and SMODS.GameStates[state].ease_background_colour then diff --git a/src/utils.lua b/src/utils.lua index 8e8876eb9..3b7091b34 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -1292,7 +1292,7 @@ SMODS.calculate_individual_effect = function(effect, scored_card, key, amount, f SMODS.calculate_context({ money_altered = true, amount = amount, - from_shop = (G.STATE == G.STATES.SHOP or G.STATE == G.STATES.SMODS_BOOSTER_OPENED or G.STATE == G.STATES.SMODS_REDEEM_VOUCHER) or nil, + from_shop = (G.STATE == G.STATES.SHOP or G.STATE == SMODS.STATES.BOOSTER_OPENED or G.STATE == SMODS.STATES.REDEEM_VOUCHER) or nil, from_consumeable = (G.STATE == G.STATES.PLAY_TAROT) or nil, from_scoring = (G.STATE == G.STATES.HAND_PLAYED) or nil, }) @@ -2819,7 +2819,7 @@ function SMODS.update_hand_limit_text(play, discard) end function SMODS.draw_cards(hand_space) - if not (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.SMODS_BOOSTER_OPENED) and + if not (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED) and G.hand.config.card_limit <= 0 and #G.hand.cards == 0 then G.STATE = G.STATES.GAME_OVER; G.STATE_COMPLETE = false return true @@ -3314,7 +3314,7 @@ function ease_dollars(mod, instant) SMODS.calculate_context({ money_altered = true, amount = mod, - from_shop = (G.STATE == G.STATES.SHOP or G.STATE == G.STATES.SMODS_BOOSTER_OPENED or G.STATE == G.STATES.SMODS_REDEEM_VOUCHER) or nil, + from_shop = (G.STATE == G.STATES.SHOP or G.STATE == SMODS.STATES.BOOSTER_OPENED or G.STATE == SMODS.STATES.REDEEM_VOUCHER) or nil, from_consumeable = (G.STATE == G.STATES.PLAY_TAROT) or nil, from_scoring = (G.STATE == G.STATES.HAND_PLAYED) or nil, }) From d2f28cad6e9288ba2318a8bcb3344d63dbbae0ee Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Sat, 4 Apr 2026 13:35:57 +0200 Subject: [PATCH 06/39] Still WIP *Title --- lovely/blind.toml | 38 ------------ lovely/weights.toml | 14 ----- src/game_object.lua | 137 ++++++++++++++++++++++++++++++++++++++++++++ src/overrides.lua | 135 ++----------------------------------------- src/utils.lua | 4 +- 5 files changed, 145 insertions(+), 183 deletions(-) diff --git a/lovely/blind.toml b/lovely/blind.toml index 2ba89aa57..1f38a8414 100644 --- a/lovely/blind.toml +++ b/lovely/blind.toml @@ -462,44 +462,6 @@ payload = ''' {n=G.UIT.O, config={object = DynaText({string = localize(target), colours = {G.C.WHITE},shadow = true, float = true,maxw = 2.2, scale = 0.45})}} ''' -# get_new_boss() -[[patches]] -[patches.pattern] -target = 'functions/common_events.lua' -pattern = ''' -if not v.boss then - -elseif not v.boss.showdown and (v.boss.min <= math.max(1, G.GAME.round_resets.ante) and ((math.max(1, G.GAME.round_resets.ante))%G.GAME.win_ante ~= 0 or G.GAME.round_resets.ante < 2)) then - eligible_bosses[k] = true -elseif v.boss.showdown and (G.GAME.round_resets.ante)%G.GAME.win_ante == 0 and G.GAME.round_resets.ante >= 2 then - eligible_bosses[k] = true -end -''' -match_indent = true -position = 'at' -payload = ''' -local res, options = SMODS.add_to_pool(v) -options = options or {} -if not v.boss then - -elseif options.ignore_showdown_check then - eligible_bosses[k] = res and true or nil -elseif v.in_pool and type(v.in_pool) == 'function' then - if - ( - ((G.GAME.round_resets.ante)%G.GAME.win_ante == 0 and G.GAME.round_resets.ante >= 2) == - (v.boss.showdown or false) - ) - then - eligible_bosses[k] = res and true or nil - end -elseif not v.boss.showdown and (v.boss.min <= math.max(1, G.GAME.round_resets.ante) and ((math.max(1, G.GAME.round_resets.ante))%G.GAME.win_ante ~= 0 or G.GAME.round_resets.ante < 2)) then - eligible_bosses[k] = res and true or nil -elseif v.boss.showdown and (G.GAME.round_resets.ante)%G.GAME.win_ante == 0 and G.GAME.round_resets.ante >= 2 then - eligible_bosses[k] = res and true or nil -end -''' - # G.UIDEF.challenge_description_tab [[patches]] [patches.pattern] diff --git a/lovely/weights.toml b/lovely/weights.toml index 3df1fd5b5..1298e9602 100644 --- a/lovely/weights.toml +++ b/lovely/weights.toml @@ -3,20 +3,6 @@ version = "1.0.0" dump_lua = true priority = -10 -# Bypass get_new_boss() -[[patches]] -[patches.pattern] -target = 'functions/common_events.lua' -match_indent = true -position = 'after' -pattern = ''' -function get_new_boss() -''' -payload = ''' --- Use SMODS object weight system when enabled -if SMODS.optional_features.object_weights then return SMODS.poll_object({type = 'Blind'}) end -''' - # Bypass create_card_for_shop() [[patches]] [patches.pattern] diff --git a/src/game_object.lua b/src/game_object.lua index ac295ad05..b90de6f3a 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -1833,6 +1833,140 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. return self.blind_types end } + --[[ + blind_states and loc_blind_states + + Game:update(dt) -> This; + if G.prev_small_state ~= G.GAME.round_resets.blind_states.Small or + G.prev_large_state ~= G.GAME.round_resets.blind_states.Big or + G.prev_boss_state ~= G.GAME.round_resets.blind_states.Boss or G.RESET_BLIND_STATES then ... + can probably be ignored and replaced with own system + + Game:start_run() -> get_next_tag_key() is called, besides that; blind_states is set + ]] + function reset_blinds() + G.GAME.round_resets.blind_states = G.GAME.round_resets.blind_states or {Small = 'Select', Big = 'Upcoming', Boss = 'Upcoming'} + if G.GAME.round_resets.blind_states.Boss == 'Defeated' then + G.GAME.round_resets.blind_states.Small = 'Upcoming' + G.GAME.round_resets.blind_states.Big = 'Upcoming' + G.GAME.round_resets.blind_states.Boss = 'Upcoming' + G.GAME.blind_on_deck = 'Small' + G.GAME.round_resets.blind_choices.Boss = SMODS.get_new_blind({Boss = true}) + G.GAME.round_resets.boss_rerolled = false + end + end + + function SMODS.get_blind_types(blind_obj) + if type(blind_obj.get_types) == "function" then + return blind_obj:get_types() + else + if v.boss then + if not v.boss.showdown then + return {Boss = true} + else + return {Showdown = true} + end + else + if v.name == "Small Blind" then + return {Small = true} + else + return {Big = true} + end + end + end + end + + function get_new_boss() + sendWarnMessage("get_new_boss() is deprecated; Call SMODS.get_new_blind() instead.", "utils") + end + + function SMODS.get_new_blind(blind_types) + -- Use SMODS object weight system when enabled + if SMODS.optional_features.object_weights then return SMODS.poll_object({type = 'Blind'}) end + if not blind_types or (blind_types.Boss or blind_types.Showdown) then + G.GAME.perscribed_bosses = G.GAME.perscribed_bosses or {} + if G.GAME.perscribed_bosses and G.GAME.perscribed_bosses[G.GAME.round_resets.ante] then + local ret_boss = G.GAME.perscribed_bosses[G.GAME.round_resets.ante] + G.GAME.perscribed_bosses[G.GAME.round_resets.ante] = nil + G.GAME.bosses_used[ret_boss] = G.GAME.bosses_used[ret_boss] + 1 + return ret_boss + end + if G.FORCE_BOSS then return G.FORCE_BOSS end + end + + local eligible_bosses = {} + for k, v in pairs(G.P_BLINDS) do + local res = SMODS.add_to_pool(v) + if res then + local b_types = SMODS.get_blind_types(v) + for _, b_type in pairs(b_types) do + if blind_types[b_type] then + if v.in_pool or not v.boss or (v.boss.min <= math.max(1, G.GAME.round_resets.ante) and ((math.max(1, G.GAME.round_resets.ante))%G.GAME.win_ante ~= 0 or G.GAME.round_resets.ante < 2)) then + eligible_bosses[k] = true + break + end + end + end + end + end + for k, v in pairs(G.GAME.banned_keys) do + if eligible_bosses[k] then eligible_bosses[k] = nil end + end + + local min_use = 100 + for k, v in pairs(G.GAME.bosses_used) do + if eligible_bosses[k] then + eligible_bosses[k] = v + if eligible_bosses[k] <= min_use then + min_use = eligible_bosses[k] + end + end + end + for k, v in pairs(eligible_bosses) do + if eligible_bosses[k] then + if eligible_bosses[k] > min_use then + eligible_bosses[k] = nil + end + end + end + local _, boss = pseudorandom_element(eligible_bosses, pseudoseed('boss')) + G.GAME.bosses_used[boss] = G.GAME.bosses_used[boss] + 1 + + return boss + end + + function Blind:get_type() + return SMODS.get_blind_types(self.config.blind) + end + + function Blind:is_type(b_type) + return self:is_types({[b_type] = true}, false) + end + + function Blind:is_types(b_types_map, all) + for k, v in pairs(self:get_type()) do + if v and b_types_map[k] or (k == "Showdown" and b_types_map.Boss) then + if not all then + return true + end + elseif all then + return false + end + end + return all + end + + SMODS.Joker:take_ownership("j_matador", { + check_for_unlock = function (self, args) + return G.GAME.current_round.hands_played == 1 and G.GAME.current_round.discards_left == G.GAME.round_resets.discards and G.GAME.blind:is_type("Boss") + end + }) + + SMODS.Joker:take_ownership("j_hanging_chad", { + check_for_unlock = function (self, args) + return G.GAME.last_hand_played == self.unlock_condition.extra and G.GAME.blind:is_type("Boss") + end + }) SMODS.Blind:take_ownership('eye', { set_blind = function(self, reset, silent) if not reset then @@ -4064,6 +4198,9 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. required_parameters = { 'key', }, + inject = function (self, i) + + end, on_enter = function (self, args, from_hold) end, on_exit = function (self, args, from_hold) end, update = function (self, dt) end, diff --git a/src/overrides.lua b/src/overrides.lua index ddc33fd10..3219ec198 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -2724,10 +2724,14 @@ end -- SMODS.GameState related overrides -G.FUNCS.toggle_shop = function(e) +function G.FUNCS.toggle_shop(e) SMODS.enter_state(SMODS.STATES.BLIND_SELECT) end +function G.FUNCS.cash_out(e) + SMODS.enter_state(SMODS.STATES.SHOP) +end + local ease_bg_col_bl_ref = ease_background_colour_blind function ease_background_colour_blind(state, blind_override) if SMODS.GameStates[state] and SMODS.GameStates[state].ease_background_colour then @@ -2840,9 +2844,7 @@ function end_round() trigger = 'after', delay = 0.3, func = function() - if SMODS.get_current_state().args.trigger_callbacks then - SMODS.get_active_ap_node():trigger_callbacks("defeated") - end + SMODS.enter_state(SMODS.STATES.ROUND_EVAL) G.STATE_COMPLETE = false if G.GAME.round_resets.blind == G.P_BLINDS.bl_small then @@ -2883,130 +2885,5 @@ function end_round() })) end ---[[ -blind_states and loc_blind_states - -Game:update(dt) -> This; - if G.prev_small_state ~= G.GAME.round_resets.blind_states.Small or - G.prev_large_state ~= G.GAME.round_resets.blind_states.Big or - G.prev_boss_state ~= G.GAME.round_resets.blind_states.Boss or G.RESET_BLIND_STATES then ... -can probably be ignored and replaced with own system - -Game:start_run() -> get_next_tag_key() is called, besides that; blind_states is set -]] -function reset_blinds() - G.GAME.round_resets.boss_rerolled = false -end - -function SMODS.get_blind_types(blind_obj) - if type(blind_obj.get_types) == "function" then - return blind_obj:get_types() - else - if v.boss then - if not v.boss.showdown then - return {Boss = true} - else - return {Showdown = true} - end - else - if v.name == "Small Blind" then - return {Small = true} - else - return {Big = true} - end - end - end -end - -function get_new_boss() - sendWarnMessage("get_new_boss() is deprecated; Call SMODS.get_new_blind() instead.", "utils") -end - -function SMODS.get_new_blind(blind_types) - if not blind_types or (blind_types.Boss or blind_types.Showdown) then - G.GAME.perscribed_bosses = G.GAME.perscribed_bosses or {} - if G.GAME.perscribed_bosses and G.GAME.perscribed_bosses[G.GAME.round_resets.ante] then - local ret_boss = G.GAME.perscribed_bosses[G.GAME.round_resets.ante] - G.GAME.perscribed_bosses[G.GAME.round_resets.ante] = nil - G.GAME.bosses_used[ret_boss] = G.GAME.bosses_used[ret_boss] + 1 - return ret_boss - end - if G.FORCE_BOSS then return G.FORCE_BOSS end - end - - local eligible_bosses = {} - for k, v in pairs(G.P_BLINDS) do - local res = SMODS.add_to_pool(v) - if res then - local b_types = SMODS.get_blind_types(v) - for _, b_type in pairs(b_types) do - if blind_types[b_type] then - if v.in_pool or not v.boss or (v.boss.min <= math.max(1, G.GAME.round_resets.ante) and ((math.max(1, G.GAME.round_resets.ante))%G.GAME.win_ante ~= 0 or G.GAME.round_resets.ante < 2)) then - eligible_bosses[k] = true - break - end - end - end - end - end - for k, v in pairs(G.GAME.banned_keys) do - if eligible_bosses[k] then eligible_bosses[k] = nil end - end - - local min_use = 100 - for k, v in pairs(G.GAME.bosses_used) do - if eligible_bosses[k] then - eligible_bosses[k] = v - if eligible_bosses[k] <= min_use then - min_use = eligible_bosses[k] - end - end - end - for k, v in pairs(eligible_bosses) do - if eligible_bosses[k] then - if eligible_bosses[k] > min_use then - eligible_bosses[k] = nil - end - end - end - local _, boss = pseudorandom_element(eligible_bosses, pseudoseed('boss')) - G.GAME.bosses_used[boss] = G.GAME.bosses_used[boss] + 1 - - return boss -end - -function Blind:get_type() - return SMODS.get_blind_types(self.config.blind) -end - -function Blind:is_type(b_type) - return self:is_types({[b_type] = true}, false) -end - -function Blind:is_types(b_types_map, all) - for k, v in pairs(self:get_type()) do - if v and b_types_map[k] or (k == "Showdown" and b_types_map.Boss) then - if not all then - return true - end - elseif all then - return false - end - end - return all -end - -SMODS.Joker:take_ownership("j_matador", { - check_for_unlock = function (self, args) - return G.GAME.current_round.hands_played == 1 and G.GAME.current_round.discards_left == G.GAME.round_resets.discards and G.GAME.blind:is_type("Boss") - end -}) - -SMODS.Joker:take_ownership("j_hanging_chad", { - check_for_unlock = function (self, args) - return G.GAME.last_hand_played == self.unlock_condition.extra and G.GAME.blind:is_type("Boss") - end -}) - -- Create Blind Select UI -> Not used in SMODS.STATES.BLIND_SELECT.on_enter() function create_UIBox_blind_select() end \ No newline at end of file diff --git a/src/utils.lua b/src/utils.lua index 3b7091b34..b17c29dae 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -1292,7 +1292,7 @@ SMODS.calculate_individual_effect = function(effect, scored_card, key, amount, f SMODS.calculate_context({ money_altered = true, amount = amount, - from_shop = (G.STATE == G.STATES.SHOP or G.STATE == SMODS.STATES.BOOSTER_OPENED or G.STATE == SMODS.STATES.REDEEM_VOUCHER) or nil, + from_shop = (G.STATE == SMODS.STATES.SHOP or G.STATE == SMODS.STATES.BOOSTER_OPENED or G.STATE == SMODS.STATES.REDEEM_VOUCHER) or nil, from_consumeable = (G.STATE == G.STATES.PLAY_TAROT) or nil, from_scoring = (G.STATE == G.STATES.HAND_PLAYED) or nil, }) @@ -3314,7 +3314,7 @@ function ease_dollars(mod, instant) SMODS.calculate_context({ money_altered = true, amount = mod, - from_shop = (G.STATE == G.STATES.SHOP or G.STATE == SMODS.STATES.BOOSTER_OPENED or G.STATE == SMODS.STATES.REDEEM_VOUCHER) or nil, + from_shop = (G.STATE == SMODS.STATES.SHOP or G.STATE == SMODS.STATES.BOOSTER_OPENED or G.STATE == SMODS.STATES.REDEEM_VOUCHER) or nil, from_consumeable = (G.STATE == G.STATES.PLAY_TAROT) or nil, from_scoring = (G.STATE == G.STATES.HAND_PLAYED) or nil, }) From fcc73179a709ca64325690b7012910bd12851691 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Sat, 4 Apr 2026 17:38:09 +0200 Subject: [PATCH 07/39] Made stuff functional +/*Title, huge news (and commit) --- lovely/booster.toml | 156 +------------- lovely/center.toml | 44 ---- lovely/compact_cashout.toml | 40 ---- lovely/game_state.toml | 22 +- src/game_object.lua | 174 ++++++++++++---- src/overrides.lua | 401 +++++++++++++++++++++++++++++++++++- 6 files changed, 542 insertions(+), 295 deletions(-) diff --git a/lovely/booster.toml b/lovely/booster.toml index 516bafeeb..fcf7a8d8c 100644 --- a/lovely/booster.toml +++ b/lovely/booster.toml @@ -34,7 +34,7 @@ payload = """ booster_obj = self.config.center if booster_obj and SMODS.Centers[booster_obj.key] then SMODS.OPENED_BOOSTER = self - SMODS.enter_state(SMODS.STATES.BOOSTER_OPENED) + SMODS.enter_state(SMODS.STATES.BOOSTER_OPENED, nil, true) end G.GAME.pack_choices = math.min((self.ability.choose or self.config.center.config.choose or 1) + (G.GAME.modifiers.booster_choice_mod or 0), self.ability.extra and math.max(1, self.ability.extra + (G.GAME.modifiers.booster_size_mod or 0)) or self.config.center.extra and math.max(1, self.config.center.extra + (G.GAME.modifiers.booster_size_mod or 0)) or 1) """ @@ -89,7 +89,7 @@ payload = ''' SMODS.calculate_context({modify_booster_card = true, booster = self, card = card, index = i}) ''' -# G.FUNC.can_skip_booster +# G.FUNCS.can_skip_booster # TODO customize whether pack can be skipped [[patches]] [patches.regex] @@ -108,16 +108,6 @@ payload = ''' (self.config.type == 'hand' and state == SMODS.STATES.BOOSTER_OPENED) or''' match_indent = true -# G.FUNCS.use_card -[[patches]] -[patches.pattern] -target = "functions/button_callbacks.lua" -pattern = "prev_state == G.STATES.SPECTRAL_PACK or prev_state == G.STATES.STANDARD_PACK or" -position = "after" -payload = ''' -prev_state == SMODS.STATES.BOOSTER_OPENED or''' -match_indent = true - # CardArea:align_cards() [[patches]] [patches.pattern] @@ -145,87 +135,6 @@ position = "at" payload = "if G.STATE == G.STATES.SELECTING_HAND or G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED then" match_indent = true -# G.FUNCS.use_card() -[[patches]] -[patches.pattern] -target = "functions/button_callbacks.lua" -pattern = "if G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.SPECTRAL_PACK then" -position = "at" -payload = """ -if nc then - if area then area:remove_from_highlighted(card) end - play_sound('cardSlide2', nil, 0.3) - dont_dissolve = true -end -if (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED) then""" -match_indent = true - -# G.FUNC.use_card() -[[patches]] -[patches.pattern] -target = "functions/button_callbacks.lua" -pattern = 'if area == G.consumeables then' -position = 'before' -match_indent = true -payload = ''' -if nc and area == G.pack_cards and not select_to then G.pack_cards:remove_card(card); G.consumeables:emplace(card) end''' - -# G.FUNC.use_card() -[[patches]] -[patches.pattern] -target = "functions/button_callbacks.lua" -pattern = '''if prev_state == G.STATES.TAROT_PACK then inc_career_stat('c_tarot_reading_used', 1) end''' -position = 'at' -match_indent = true -payload = '''if prev_state == SMODS.STATES.BOOSTER_OPENED and booster_obj.name:find('Arcana') then inc_career_stat('c_tarot_reading_used', 1) end''' - -# G.FUNC.use_card() -[[patches]] -[patches.pattern] -target = "functions/button_callbacks.lua" -pattern = '''if prev_state == G.STATES.PLANET_PACK then inc_career_stat('c_planetarium_used', 1) end''' -position = 'at' -match_indent = true -payload = '''if prev_state == SMODS.STATES.BOOSTER_OPENED and booster_obj.name:find('Celestial') then inc_career_stat('c_planetarium_used', 1) end''' - -# G.FUNC.use_card() -[[patches]] -[patches.pattern] -target = "functions/button_callbacks.lua" -pattern = "(G.STATE == G.STATES.BUFFOON_PACK and G.STATES.BUFFOON_PACK) or" -position = "before" -payload = "(G.STATE == SMODS.STATES.BOOSTER_OPENED and SMODS.STATES.BOOSTER_OPENED) or" -match_indent = true - -# G.FUNC.use_card() -[[patches]] -[patches.pattern] -target = "functions/state_events.lua" -pattern = "if not (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK) and" -position = "at" -payload = "if not (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED) and" -match_indent = true - -# Card:use_consumeable() -[[patches]] -[patches.regex] -target = "card.lua" -pattern = '''(?[\t ]*)align = \(G\.STATE[\s\S]*and -0\.2 or 0},''' -position = "at" -payload = ''' -align = (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED) and 'tm' or 'cm', -offset = {x = 0, y = (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED) and -0.2 or 0},''' -line_prepend = '$indent' - -# G.FUNCS.use_card() -[[patches]] -[patches.pattern] -target = "functions/button_callbacks.lua" -pattern = "e.config.ref_table:redeem()" -position = "before" -payload = "if area == G.pack_cards then e.config.ref_table.cost = 0 end" -match_indent = true - ## Stopping ease_dollars anim from playing when voucher is free # Card:redeem() [[patches]] @@ -264,50 +173,6 @@ if card.ability.consumeable and card.area == G.pack_cards and G.pack_cards and b end ''' -# G.FUNCS.use_card() -[[patches]] -[patches.pattern] -target = 'functions/button_callbacks.lua' -match_indent = true -position = 'at' -pattern = ''' -if card.ability.consumeable then - if nc then -''' -payload = ''' -if select_to then - card:add_to_deck() - G[select_to]:emplace(card) - if card.config.center.on_select and type(card.config.center.on_select) == 'function' then - card.config.center:on_select(card) - end - play_sound('card1', 0.8, 0.6) - play_sound('generic1') - dont_dissolve = true - delay_fac = 0.2 -elseif card.ability.consumeable then - if nc then -''' -# G.FUNCS.end_consumeable() -[[patches]] -[patches.pattern] -target = 'functions/button_callbacks.lua' -match_indent = true -position = 'before' -pattern = ''' -for i = 1, #G.GAME.tags do - if G.GAME.tags[i]:apply_to_run({type = 'new_blind_choice'}) then break end -end - -G.E_MANAGER:add_event(Event({trigger = 'after',delay = 0.2*delayfac, - func = function() - save_run() - return true - end})) -''' -payload = ''' -booster_obj = nil -''' # G.FUNCS.skip_booster() [[patches]] [patches.pattern] @@ -341,19 +206,4 @@ target = 'card.lua' match_indent = true position = 'after' pattern = '''if self.shop_voucher then G.GAME.current_round.voucher = nil end''' -payload = '''G.STATE = SMODS.STATES.REDEEM_VOUCHER''' - -# G.FUNCS.use_card() -# Consumables in areas other than the consumable don't count -# as picking an item in boosters when used -[[patches]] -[patches.pattern] -target = 'functions/button_callbacks.lua' -match_indent = true -position = 'at' -pattern = ''' -if area == G.consumeables then -''' -payload = ''' -if area ~= G.pack_cards then -''' +payload = '''SMODS.enter_state(SMODS.STATES.REDEEM_VOUCHER, nil, true)''' diff --git a/lovely/center.toml b/lovely/center.toml index 99db6a996..aa16b7a52 100644 --- a/lovely/center.toml +++ b/lovely/center.toml @@ -526,50 +526,6 @@ if obj and obj.remove_from_deck and type(obj.remove_from_deck) == 'function' the obj:remove_from_deck(self, from_debuff) end''' -# G.FUNCS.use_card() -[[patches]] -[patches.pattern] -target = 'functions/button_callbacks.lua' -match_indent = true -position = 'at' -pattern = ''' -if G.booster_pack and not G.booster_pack.alignment.offset.py and (card.ability.consumeable or not (G.GAME.pack_choices and G.GAME.pack_choices > 1)) then -''' -payload = ''' -local nc -local select_to = card.area == G.pack_cards and G.pack_cards and booster_obj and SMODS.card_select_area(card, booster_obj) and card:selectable_from_pack(booster_obj) -if card.ability.consumeable and not select_to then - local obj = card.config.center - if obj.keep_on_use and type(obj.keep_on_use) == 'function' then - nc = obj:keep_on_use(card) - end -end -if G.booster_pack and not G.booster_pack.alignment.offset.py and ((not select_to and card.ability.consumeable) or not (G.GAME.pack_choices and G.GAME.pack_choices > 1)) then - -''' - - -# G.FUNCS.use_card() -[[patches]] -[patches.pattern] -target = 'functions/button_callbacks.lua' -pattern = "if card.area then card.area:remove_card(card) end" -match_indent = true -position = 'at' -payload = ''' -if not card.from_area then card.from_area = card.area end -if card.area and (not nc or card.area == G.pack_cards) then card.area:remove_card(card) end''' -[[patches]] -[patches.pattern] -target = 'functions/button_callbacks.lua' -match_indent = true -position = 'before' -pattern = ''' -if area and area.cards[1] then -''' -payload = ''' -if nc and not area then G.consumeables:emplace(card) end -''' [[patches]] [patches.pattern] target = 'functions/button_callbacks.lua' diff --git a/lovely/compact_cashout.toml b/lovely/compact_cashout.toml index 98676f316..960607e0d 100644 --- a/lovely/compact_cashout.toml +++ b/lovely/compact_cashout.toml @@ -22,44 +22,4 @@ payload = ''' end''' match_indent = true -# Reset rows amount - -[[patches]] -[patches.regex] -target = "functions/state_events.lua" -pattern = 'G\.FUNCS\.evaluate_round = function\(\)' -position = "after" -payload = ''' - - total_cashout_rows = 0''' - -# Add UI row with total rows hidden - -[[patches]] -[patches.pattern] -target = "functions/state_events.lua" -pattern = "add_round_eval_row({name = 'bottom', dollars = dollars})" -position = "before" -payload = ''' -if total_cashout_rows > 7 then - local total_hidden = total_cashout_rows - 7 - - G.E_MANAGER:add_event(Event({ - trigger = 'before',delay = 0.38, - func = function() - local hidden = {n=G.UIT.R, config={align = "cm"}, nodes={ - {n=G.UIT.O, config={object = DynaText({ - string = {localize{type = 'variable', key = 'cashout_hidden', vars = {total_hidden}}}, - colours = {G.C.WHITE}, shadow = true, float = false, - scale = 0.45, - font = G.LANGUAGES['en-us'].font, pop_in = 0 - })}} - }} - - G.round_eval:add_child(hidden, G.round_eval:get_UIE_by_ID('bonus_round_eval')) - return true - end - })) -end''' -match_indent = true diff --git a/lovely/game_state.toml b/lovely/game_state.toml index 1952b72fd..b0739526c 100644 --- a/lovely/game_state.toml +++ b/lovely/game_state.toml @@ -28,37 +28,29 @@ target = 'game.lua' match_indent = true position = 'at' pattern = ''' - self:prep_stage(G.STAGES.RUN, saveTable and saveTable.STATE or G.STATES.BLIND_SELECT) +self:prep_stage(G.STAGES.RUN, saveTable and saveTable.STATE or G.STATES.BLIND_SELECT) ''' payload = ''' self:prep_stage(G.STAGES.RUN, saveTable and saveTable.STATE or SMODS.STATES.BLIND_SELECT) ''' +# Game:prep_stage() [[patches]] [patches.pattern] target = 'game.lua' match_indent = true position = 'at' pattern = ''' - self:prep_stage(G.STAGES.RUN, saveTable and saveTable.STATE or G.STATES.BLIND_SELECT) +self.STATE = new_state or self.STATES.MENU ''' payload = ''' -self:prep_stage(G.STAGES.RUN, saveTable and saveTable.STATE or SMODS.STATES.BLIND_SELECT) +SMODS.enter_state(new_state or SELF.STATES.MENU) ''' -# functions/button_callbacks.lua : G.FUNCS.use_card() : update logic -[[patches]] -[patches.pattern] -target = 'functions/button_callbacks.lua' -pattern = '''if card.ability.set == 'Booster' and not nosave and G.STATE == G.STATES.SHOP then''' -position = 'at' -match_indent = true -payload = '''if card.ability.set == 'Booster' and not nosave and G.STATE == SMODS.STATES.SHOP then''' - -# functions/button_callbacks.lua : G.FUNCS.use_card() : update logic +# cardarea.lua : CardArea:draw() : update logic [[patches]] [patches.pattern] -target = 'functions/button_callbacks.lua' +target = 'cardarea.lua' pattern = '''(self.config.type == 'hand' and ({[G.STATES.SHOP]=1, [G.STATES.TAROT_PACK]=1, [G.STATES.SPECTRAL_PACK]=1, [G.STATES.STANDARD_PACK]=1,[G.STATES.BUFFOON_PACK]=1,[G.STATES.PLANET_PACK]=1, [G.STATES.ROUND_EVAL]=1, [G.STATES.BLIND_SELECT]=1})[state]) or''' position = 'at' match_indent = true -payload = '''(self.config.type == 'hand' and ({[SMODS.STATES.SHOP]=1, [G.STATES.TAROT_PACK]=1, [G.STATES.SPECTRAL_PACK]=1, [G.STATES.STANDARD_PACK]=1,[G.STATES.BUFFOON_PACK]=1,[G.STATES.PLANET_PACK]=1, [SMODS.STATES.ROUND_EVAL]=1, [SMODS.STATES.BLIND_SELECT]=1})[state]) or''' \ No newline at end of file +payload = '''(self.config.type == 'hand' and ({[SMODS.STATES.SHOP]=1, [G.STATES.TAROT_PACK]=1, [G.STATES.SPECTRAL_PACK]=1, [G.STATES.STANDARD_PACK]=1,[G.STATES.BUFFOON_PACK]=1,[G.STATES.PLANET_PACK]=1, [SMODS.STATES.ROUND_EVAL]=1, [SMODS.STATES.BLIND_SELECT]=1})[state]) or''' diff --git a/src/game_object.lua b/src/game_object.lua index b90de6f3a..28f976965 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -1857,27 +1857,32 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. end function SMODS.get_blind_types(blind_obj) + local ret if type(blind_obj.get_types) == "function" then - return blind_obj:get_types() - else - if v.boss then - if not v.boss.showdown then + ret = blind_obj:get_types() + end + if not ret then + if blind_obj.boss then + if not blind_obj.boss.showdown then return {Boss = true} else return {Showdown = true} end else - if v.name == "Small Blind" then + if blind_obj.name == "Small Blind" then return {Small = true} else return {Big = true} end end end + return ret end function get_new_boss() - sendWarnMessage("get_new_boss() is deprecated; Call SMODS.get_new_blind() instead.", "utils") + -- sendWarnMessage("get_new_boss() is deprecated; Call SMODS.get_new_blind() instead.", "utils") + local boss = SMODS.get_new_blind({Boss = true}) + return boss end function SMODS.get_new_blind(blind_types) @@ -1899,7 +1904,7 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. local res = SMODS.add_to_pool(v) if res then local b_types = SMODS.get_blind_types(v) - for _, b_type in pairs(b_types) do + for b_type, _ in pairs(b_types) do if blind_types[b_type] then if v.in_pool or not v.boss or (v.boss.min <= math.max(1, G.GAME.round_resets.ante) and ((math.max(1, G.GAME.round_resets.ante))%G.GAME.win_ante ~= 0 or G.GAME.round_resets.ante < 2)) then eligible_bosses[k] = true @@ -4151,6 +4156,10 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. end end + function SMODS.clear_state_stack() + SMODS.state_stack = {} + end + function SMODS.clear_states(exempt_map) exempt_map = exempt_map or {} if G.blind_select and not exempt_map[SMODS.STATES.BLIND_SELECT] then G.blind_select:remove(); G.blind_select = nil end @@ -4159,37 +4168,50 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. if G.round_eval and not exempt_map[SMODS.STATES.ROUND_EVAL] then G.round_eval:remove(); G.round_eval = nil end end - function SMODS.enter_state(state, args, hold_state) - if G.STATE == state then return end - if SMODS.GameStates[G.STATE] and hold_state then - SMODS.GameStates[G.STATE]:on_exit({new_state=state}, true) - end - if not hold_state then - SMODS.pop_from_state_stack(G.STATE) + function SMODS.enter_state(state, args, do_hold_state) + local previous_state = SMODS.STATE or G.STATE -- It only handles on_exit() and on_enter() for SMODS.GameState states + if previous_state == state then return end + if SMODS.GameStates[previous_state] then + SMODS.GameStates[previous_state]:on_exit({new_state=state}, do_hold_state) + if not do_hold_state then + SMODS.pop_from_state_stack(previous_state) + end end G.STATE = state - if SMODS.GameStates[G.STATE] then - SMODS.GameStates[G.STATE]:on_enter(args) + if SMODS.GameStates[state] then + SMODS.STATE = state + SMODS.GameStates[state]:on_enter(args) + SMODS.push_to_state_stack(state, args) end - SMODS.push_to_state_stack(state, args) end - function SMODS.exit_state(args) - if SMODS.GameStates[G.STATE] then - SMODS.GameStates[G.STATE]:on_exit(args) + function SMODS.exit_state(args, default_state_override) + local current_state = SMODS.STATE or G.STATE + if SMODS.GameStates[current_state] then + SMODS.GameStates[current_state]:on_exit(args) + SMODS.pop_from_state_stack(current_state) end - SMODS.pop_from_state_stack(G.STATE) - if #SMODS.context_stack < 1 then + if #SMODS.state_stack < 1 then G.STATE = nil - SMODS.enter_state(SMODS.default_state) + SMODS.STATE = nil + SMODS.enter_state(default_state_override or SMODS.default_state) return end - G.STATE = SMODS.context_stack[#SMODS.context_stack].state + G.STATE = SMODS.state_stack[#SMODS.state_stack].state if SMODS.GameStates[G.STATE] then + SMODS.STATE = G.STATE SMODS.GameStates[G.STATE]:on_enter(args, true) end end + local delete_run_ref = Game.delete_run + function Game:delete_run() + local ret = delete_run_ref(self) + SMODS.STATE = nil + SMODS.clear_state_stack() + return ret + end + SMODS.GameStates = {} SMODS.GameState = SMODS.GameObject:extend{ set = 'GameState', @@ -4205,26 +4227,38 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. on_exit = function (self, args, from_hold) end, update = function (self, dt) end, ease_background_colour = nil, -- function + exit_after_use_card = false, -- Used for consumable states like SMODS.STATES.REDEEM_VOUCHER + exit_after_end_consumable = false, -- Used for booster-like states like SMODS.STATES.BOOSTER_OPENED } SMODS.GameState { key = SMODS.STATES.BOOSTER_OPENED, update = function (self, dt) SMODS.OPENED_BOOSTER.config.center:update_pack(dt) - end + end, + exit_after_end_consumable = true, } SMODS.GameState { - key = SMODS.STATES.REDEEM_VOUCHER + key = SMODS.STATES.REDEEM_VOUCHER, + exit_after_use_card = true, } SMODS.GameState { key = SMODS.STATES.SHOP, on_enter = function (self, args, from_hold) + if from_hold then + -- Extracted from G.FUNCS.use_card() + if G.shop then + G.shop.alignment.offset.y = G.shop.alignment.offset.py + G.shop.alignment.offset.py = nil + end + return + end G.E_MANAGER:add_event(Event({ trigger = "immediate", func = function () - SMODS.clear_states({[SMODS.STATES.SHOP] = true}) + SMODS.clear_states() stop_use() G.STATE_COMPLETE = true ease_background_colour_blind(G.STATES.SHOP) @@ -4342,6 +4376,13 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. })) end, on_exit = function (self, args, from_hold) + if from_hold then + if G.shop and not G.shop.alignment.offset.py then + G.shop.alignment.offset.py = G.shop.alignment.offset.y + G.shop.alignment.offset.y = G.ROOM.T.y + 29 + end + return + end stop_use() G.CONTROLLER.locks.toggle_shop = true if G.shop then @@ -4388,6 +4429,13 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. SMODS.GameState { key = SMODS.STATES.ROUND_EVAL, on_enter = function (self, args, from_hold) + if from_hold then + if G.round_eval then + G.round_eval.alignment.offset.y = G.round_eval.alignment.offset.py + G.round_eval.alignment.offset.py = nil + end + return + end G.E_MANAGER:add_event(Event({ trigger = "immediate", func = function () @@ -4427,6 +4475,13 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. })) end, on_exit = function (self, args, from_hold) + if from_hold then + if G.round_eval and not G.round_eval.alignment.offset.py then + G.round_eval.alignment.offset.py = G.round_eval.alignment.offset.y + G.round_eval.alignment.offset.y = G.ROOM.T.y + 29 + end + return + end stop_use() if G.round_eval then G.round_eval.alignment.offset.y = G.ROOM.T.y + 15 @@ -4434,6 +4489,8 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. G.deck:shuffle('cashout'..G.GAME.round_resets.ante) G.deck:hard_set_T() delay(0.3) + G.GAME.current_round.discards_left = math.max(0, G.GAME.round_resets.discards + G.GAME.round_bonus.discards) + G.GAME.current_round.hands_left = (math.max(1, G.GAME.round_resets.hands + G.GAME.round_bonus.next_hands)) G.E_MANAGER:add_event(Event({ trigger = 'immediate', func = function() @@ -4455,7 +4512,13 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. play_sound("coin7") G.VIBRATION = G.VIBRATION + 1 end + ease_chips(0) + if G.GAME.round_resets.blind_states.Boss == 'Defeated' then + G.GAME.round_resets.blind_ante = G.GAME.round_resets.ante + G.GAME.round_resets.blind_tags.Small = get_next_tag_key() + G.GAME.round_resets.blind_tags.Big = get_next_tag_key() + end reset_blinds() delay(0.6) end, @@ -4501,25 +4564,44 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. on_exit = function (self, args, from_hold) G.GAME.facing_blind = nil if not from_hold then - -- Taken from G.FUNC.evaluate_round(), defeats blind - -- The extra nested immediate event should hopefully preserve the vanilla timing + -- Taken from G.FUNCS.evaluate_round(), defeats blind + -- The extra nested immediate events should hopefully preserve the vanilla timing G.E_MANAGER:add_event(Event({ trigger = "immediate", func = function () G.E_MANAGER:add_event(Event({ - trigger = 'before', - delay = 1.3*math.min(G.GAME.blind.dollars+2, 7)/2*0.15 + 0.5, - func = function() - G.GAME.blind:defeat() - G.GAME.current_round.discards_left = math.max(0, G.GAME.round_resets.discards + G.GAME.round_bonus.discards) - G.GAME.current_round.hands_left = (math.max(1, G.GAME.round_resets.hands + G.GAME.round_bonus.next_hands)) + trigger = "immediate", + func = function () + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + blocking = false, + func = function () + if not G.round_eval or (G.round_eval.alignment.offset.y == -7.8 and math.abs(G.round_eval.T.y - G.round_eval.VT.y) < 3) then + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + func = function () + G.E_MANAGER:add_event(Event({ + trigger = 'before', + delay = 1.3*math.min(G.GAME.blind.dollars+2, 7)/2*0.15 + 0.5, + func = function() + G.GAME.blind:defeat() + return true + end + })) + delay(0.2) + return true + end + })) + return true + end + end, + })) return true end })) return true end })) - ------ end end, @@ -4544,6 +4626,13 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. SMODS.GameState { key = SMODS.STATES.BLIND_SELECT, on_enter = function (self, args, from_hold) + if from_hold then + if G.blind_select then + G.blind_select.alignment.offset.y = G.blind_select.alignment.offset.py + G.blind_select.alignment.offset.py = nil + end + return + end G.E_MANAGER:add_event(Event({ trigger = "immediate", func = function() @@ -4557,7 +4646,10 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. trigger = 'immediate', func = function() play_sound('cancel') - G.blind_select = SMODS.get_ante_path():create_ui() + G.blind_select = UIBox{ + definition = create_UIBox_blind_select(), + config = {align="bmi", offset = {x=0,y=G.ROOM.T.y + 29},major = G.hand, bond = 'Weak'} + } G.blind_select.alignment.offset.y = 0.8-(G.hand.T.y - G.jokers.T.y) + G.blind_select.T.h G.ROOM.jiggle = G.ROOM.jiggle + 3 G.blind_select.alignment.offset.x = 0 @@ -4578,7 +4670,13 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. })) end, on_exit = function (self, args, from_hold) - -- TODO : Figure out what this was doing + if from_hold then + if G.blind_select and not G.blind_select.alignment.offset.py then + G.blind_select.alignment.offset.py = G.blind_select.alignment.offset.y + G.blind_select.alignment.offset.y = G.ROOM.T.y + 39 + end + return + end G.blind_prompt_box:get_UIE_by_ID('prompt_dynatext1').config.object.pop_delay = 0 G.blind_prompt_box:get_UIE_by_ID('prompt_dynatext1').config.object:pop_out(5) G.blind_prompt_box:get_UIE_by_ID('prompt_dynatext2').config.object.pop_delay = 0 diff --git a/src/overrides.lua b/src/overrides.lua index 3219ec198..60538fe42 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -2729,7 +2729,25 @@ function G.FUNCS.toggle_shop(e) end function G.FUNCS.cash_out(e) - SMODS.enter_state(SMODS.STATES.SHOP) + stop_use() + if G.round_eval then + e.config.button = nil + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + SMODS.enter_state(SMODS.STATES.SHOP) + G.STATE_COMPLETE = false + return true + end + })) + end +end + +function G.FUNCS.select_blind(e) + stop_use() + if G.blind_select then + SMODS.enter_state(SMODS.STATES.BLIND, {key = e.config.ref_table.key}) + end end local ease_bg_col_bl_ref = ease_background_colour_blind @@ -2837,7 +2855,7 @@ function end_round() if G.GAME.modifiers.set_joker_slots_ante and (G.GAME.round_resets.ante == G.GAME.modifiers.set_joker_slots_ante) then G.jokers.config.card_limit = 0 end - delay(0.4) + delay(0.4); ease_ante(1); delay(0.4); check_for_unlock({type = 'ante_up', ante = G.GAME.round_resets.ante + 1}) end G.FUNCS.draw_from_discard_to_deck() G.E_MANAGER:add_event(Event({ @@ -2848,7 +2866,6 @@ function end_round() G.STATE_COMPLETE = false if G.GAME.round_resets.blind == G.P_BLINDS.bl_small then - -- TODO : Check/Replace blind_states G.GAME.round_resets.blind_states.Small = 'Defeated' elseif G.GAME.round_resets.blind == G.P_BLINDS.bl_big then G.GAME.round_resets.blind_states.Big = 'Defeated' @@ -2885,5 +2902,379 @@ function end_round() })) end --- Create Blind Select UI -> Not used in SMODS.STATES.BLIND_SELECT.on_enter() -function create_UIBox_blind_select() end \ No newline at end of file +G.FUNCS.evaluate_round = function() + total_cashout_rows = 0 + local pitch = 0.95 + local dollars = 0 + + if G.GAME.chips - G.GAME.blind.chips >= 0 then + add_round_eval_row({dollars = G.GAME.blind.dollars, name='blind1', pitch = pitch}) + pitch = pitch + 0.06 + dollars = dollars + G.GAME.blind.dollars + else + add_round_eval_row({dollars = 0, name='blind1', pitch = pitch, saved = true}) + pitch = pitch + 0.06 + end + + G.E_MANAGER:add_event(Event({ + func = function() + ease_background_colour_blind(G.STATES.ROUND_EVAL, '') + return true + end + })) + SMODS.calculate_context{round_eval = true} + G.GAME.selected_back:trigger_effect({context = 'eval'}) + + if G.GAME.current_round.hands_left > 0 and not G.GAME.modifiers.no_extra_hand_money then + add_round_eval_row({dollars = G.GAME.current_round.hands_left*(G.GAME.modifiers.money_per_hand or 1), disp = G.GAME.current_round.hands_left, bonus = true, name='hands', pitch = pitch}) + pitch = pitch + 0.06 + dollars = dollars + G.GAME.current_round.hands_left*(G.GAME.modifiers.money_per_hand or 1) + end + if G.GAME.current_round.discards_left > 0 and G.GAME.modifiers.money_per_discard then + add_round_eval_row({dollars = G.GAME.current_round.discards_left*(G.GAME.modifiers.money_per_discard), disp = G.GAME.current_round.discards_left, bonus = true, name='discards', pitch = pitch}) + pitch = pitch + 0.06 + dollars = dollars + G.GAME.current_round.discards_left*(G.GAME.modifiers.money_per_discard) + end + local i = 0 + for _, area in ipairs(SMODS.get_card_areas('jokers')) do + for _, _card in ipairs(area.cards) do + local ret, ret_opts = _card:calculate_dollar_bonus() + ret_opts = ret_opts or {} + + -- TARGET: calc_dollar_bonus per card + if ret then + if not ret_opts.no_eval_row then + i = i+1 + add_round_eval_row({dollars = ret, bonus = true, name='joker'..i, pitch = pitch, card = _card, loc_opts = ret_opts}) + pitch = pitch + 0.06 + end + dollars = dollars + ret + end + end + end + i = 0 + for _, target in ipairs(SMODS.get_card_areas('individual', 'calc_dollar_bonus')) do + if type(target.object.calc_dollar_bonus) == 'function' then + local ret, ret_opts = target.object:calc_dollar_bonus() + ret_opts = ret_opts or {} + local name + + -- TARGET: calc_dollar_bonus per individual object + if ret then + if not ret_opts.no_eval_row then + if ret_opts.text then + name = text + elseif (ret_opts.set or target.set) and (ret_opts.key or target.key) then + if ret_opts.set == "Challenge" or (not ret_opts.set and target.set == "Challenge") then + name = localize(ret_opts.key or target.key, 'challenge_names') + elseif (ret_opts.set == "Mod" or (not ret_opts.set and target.set == "Mod")) and not (G.localization.descriptions.Mod or {})[ret_opts.key or target.key] then + name = (SMODS.Mods[ret_opts.key or target.key] or {}).name + else + name = localize{type = 'name_text', set = ret_opts.set or target.set, key = ret_opts.key or target.key} + end + end + i = i+1 + add_round_eval_row({dollars = ret, bonus = true, name='custom_individual'..i, pitch = pitch, text_colour = ret_opts.text_colour or G.C.FILTER, text = name or 'ERROR', text_scale = ret_opts.scale or 0.6}) + pitch = pitch + 0.06 + end + dollars = dollars + ret + end + end + end + for i = 1, #G.GAME.tags do + local ret = G.GAME.tags[i]:apply_to_run({type = 'eval'}) + if ret then + add_round_eval_row({dollars = ret.dollars, bonus = true, name='tag'..i, pitch = pitch, condition = ret.condition, pos = ret.pos, tag = ret.tag}) + pitch = pitch + 0.06 + dollars = dollars + ret.dollars + end + end + if G.GAME.dollars >= 5 and not G.GAME.modifiers.no_interest then + add_round_eval_row({bonus = true, name='interest', pitch = pitch, dollars = G.GAME.interest_amount*math.min(math.floor(G.GAME.dollars/5), G.GAME.interest_cap/5)}) + pitch = pitch + 0.06 + if (not G.GAME.seeded and not G.GAME.challenge) or SMODS.config.seeded_unlocks then + if G.GAME.interest_amount*math.min(math.floor(G.GAME.dollars/5), G.GAME.interest_cap/5) == G.GAME.interest_amount*G.GAME.interest_cap/5 then + G.PROFILES[G.SETTINGS.profile].career_stats.c_round_interest_cap_streak = G.PROFILES[G.SETTINGS.profile].career_stats.c_round_interest_cap_streak + 1 + else + G.PROFILES[G.SETTINGS.profile].career_stats.c_round_interest_cap_streak = 0 + end + end + check_for_unlock({type = 'interest_streak'}) + dollars = dollars + G.GAME.interest_amount*math.min(math.floor(G.GAME.dollars/5), G.GAME.interest_cap/5) + end + + pitch = pitch + 0.06 + + if total_cashout_rows > 7 then + local total_hidden = total_cashout_rows - 7 + + G.E_MANAGER:add_event(Event({ + trigger = 'before',delay = 0.38, + func = function() + local hidden = {n=G.UIT.R, config={align = "cm"}, nodes={ + {n=G.UIT.O, config={object = DynaText({ + string = {localize{type = 'variable', key = 'cashout_hidden', vars = {total_hidden}}}, + colours = {G.C.WHITE}, shadow = true, float = false, + scale = 0.45, + font = G.LANGUAGES['en-us'].font, pop_in = 0 + })}} + }} + + G.round_eval:add_child(hidden, G.round_eval:get_UIE_by_ID('bonus_round_eval')) + return true + end + })) + end + add_round_eval_row({name = 'bottom', dollars = dollars}) +end + +-- Todo : *maybe* turn this into a lovely patch +G.FUNCS.use_card = function(e, mute, nosave) + e.config.button = nil + local card = e.config.ref_table + local area = card.area + local prev_state = G.STATE + local dont_dissolve = nil + local delay_fac = 1 + + if card:check_use() then + G.E_MANAGER:add_event(Event({ + func = function() + e.disable_button = nil + e.config.button = 'use_card' + return true end + })) + return + end + + if card.ability.set == 'Booster' and not nosave and G.STATE == SMODS.STATES.SHOP then + save_with_action({ + type = 'use_card', + card = card.sort_id, + }) + end + + G.TAROT_INTERRUPT = G.STATE + if card.ability.set == 'Booster' then G.GAME.PACK_INTERRUPT = G.STATE end + G.STATE = (G.STATE == G.STATES.TAROT_PACK and G.STATES.TAROT_PACK) or + (G.STATE == G.STATES.PLANET_PACK and G.STATES.PLANET_PACK) or + (G.STATE == G.STATES.SPECTRAL_PACK and G.STATES.SPECTRAL_PACK) or + (G.STATE == G.STATES.STANDARD_PACK and G.STATES.STANDARD_PACK) or + (G.STATE == SMODS.STATES.BOOSTER_OPENED and SMODS.STATES.BOOSTER_OPENED) or + (G.STATE == G.STATES.BUFFOON_PACK and G.STATES.BUFFOON_PACK) or + G.STATES.PLAY_TAROT + + G.CONTROLLER.locks.use = true + local nc + local select_to = card.area == G.pack_cards and G.pack_cards and booster_obj and SMODS.card_select_area(card, booster_obj) and card:selectable_from_pack(booster_obj) + if card.ability.consumeable and not select_to then + local obj = card.config.center + if obj.keep_on_use and type(obj.keep_on_use) == 'function' then + nc = obj:keep_on_use(card) + end + end + if G.booster_pack and not G.booster_pack.alignment.offset.py and ((not select_to and card.ability.consumeable) or not (G.GAME.pack_choices and G.GAME.pack_choices > 1)) then + G.booster_pack.alignment.offset.py = G.booster_pack.alignment.offset.y + G.booster_pack.alignment.offset.y = G.ROOM.T.y + 29 + end + + if card.children.use_button then card.children.use_button:remove(); card.children.use_button = nil end + if card.children.sell_button then card.children.sell_button:remove(); card.children.sell_button = nil end + if card.children.price then card.children.price:remove(); card.children.price = nil end + + if not card.from_area then card.from_area = card.area end + if card.area and (not nc or card.area == G.pack_cards) then card.area:remove_card(card) end + + if select_to then + card:add_to_deck() + G[select_to]:emplace(card) + if card.config.center.on_select and type(card.config.center.on_select) == 'function' then + card.config.center:on_select(card) + end + play_sound('card1', 0.8, 0.6) + play_sound('generic1') + dont_dissolve = true + delay_fac = 0.2 + elseif card.ability.consumeable then + if nc then + if area then area:remove_from_highlighted(card) end + play_sound('cardSlide2', nil, 0.3) + dont_dissolve = true + end + if (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED) then + card.T.x = G.hand.T.x + G.hand.T.w/2 - card.T.w/2 + card.T.y = G.hand.T.y + G.hand.T.h/2 - card.T.h/2 - 0.5 + discover_card(card.config.center) + elseif not nc then draw_card(G.hand, G.play, 1, 'up', true, card, nil, mute) end + delay(0.2) + e.config.ref_table:use_consumeable(area) + SMODS.calculate_context({using_consumeable = true, consumeable = card, area = card.from_area}) + elseif card.ability.set == 'Enhanced' or card.ability.set == 'Default' then + G.playing_card = (G.playing_card and G.playing_card + 1) or 1 + G.deck:emplace(card) + play_sound('card1', 0.8, 0.6) + play_sound('generic1') + card.playing_card = G.playing_card + playing_card_joker_effects({card}) + card:add_to_deck() + table.insert(G.playing_cards, card) + dont_dissolve = true + delay_fac = 0.2 + elseif card.ability.set == 'Joker' then + card:add_to_deck() + G.jokers:emplace(card) + play_sound('card1', 0.8, 0.6) + play_sound('generic1') + dont_dissolve = true + delay_fac = 0.2 + elseif card.ability.set == 'Booster' then + delay(0.1) + if card.ability.booster_pos then G.GAME.current_round.used_packs[card.ability.booster_pos] = 'USED' end + draw_card(G.hand, G.play, 1, 'up', true, card, nil, true) + if not card.from_tag then + G.GAME.round_scores.cards_purchased.amt = G.GAME.round_scores.cards_purchased.amt + 1 + end + e.config.ref_table:open() + elseif card.ability.set == 'Voucher' then + delay(0.1) + draw_card(G.hand, G.play, 1, 'up', true, card, nil, true) + G.GAME.round_scores.cards_purchased.amt = G.GAME.round_scores.cards_purchased.amt + 1 + if area == G.pack_cards then e.config.ref_table.cost = 0 end + e.config.ref_table:redeem() + end + if card.ability.set == 'Booster' then + G.CONTROLLER.locks.use = false + G.TAROT_INTERRUPT = nil + else + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 0.2, + func = function() + if not dont_dissolve then card:start_dissolve() end + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 0.1, + func = function() + if SMODS.GameStates[G.STATE] and SMODS.GameStates[G.STATE].exit_after_use_card then -- SMODS.STATES.BOOSTER_OPENED is handled by G.FUNCS.end_consumeable() below + SMODS.exit_state(nil, prev_state) + elseif not SMODS.GameStates[G.STATE] then + G.STATE = prev_state + end + G.TAROT_INTERRUPT=nil + G.CONTROLLER.locks.use = false + + if (prev_state == G.STATES.TAROT_PACK or prev_state == G.STATES.PLANET_PACK or + prev_state == G.STATES.SPECTRAL_PACK or prev_state == G.STATES.STANDARD_PACK or + prev_state == SMODS.STATES.BOOSTER_OPENED or + prev_state == G.STATES.BUFFOON_PACK) and G.booster_pack then + if nc and area == G.pack_cards and not select_to then G.pack_cards:remove_card(card); G.consumeables:emplace(card) end + if area ~= G.pack_cards then + G.booster_pack.alignment.offset.y = G.booster_pack.alignment.offset.py + G.booster_pack.alignment.offset.py = nil + elseif G.GAME.pack_choices and G.GAME.pack_choices > 1 then + if G.booster_pack.alignment.offset.py then + G.booster_pack.alignment.offset.y = G.booster_pack.alignment.offset.py + G.booster_pack.alignment.offset.py = nil + end + G.GAME.pack_choices = G.GAME.pack_choices - 1 + else + G.CONTROLLER.interrupt.focus = true + if prev_state == SMODS.STATES.BOOSTER_OPENED and booster_obj.name:find('Arcana') then inc_career_stat('c_tarot_reading_used', 1) end + if prev_state == SMODS.STATES.BOOSTER_OPENED and booster_obj.name:find('Celestial') then inc_career_stat('c_planetarium_used', 1) end + G.FUNCS.end_consumeable(nil, delay_fac) + end + else + if nc and not area then G.consumeables:emplace(card) end + if area and area.cards[1] then + G.E_MANAGER:add_event(Event({ + func = function() + G.E_MANAGER:add_event(Event({ + func = function() + G.CONTROLLER.interrupt.focus = nil + if card.ability.set == 'Voucher' then + G.CONTROLLER:snap_to({node = G.shop:get_UIE_by_ID('next_round_button')}) + elseif area then + G.CONTROLLER:recall_cardarea_focus(area) + end + return true + end + })) + return true + end + })) + end + end + return true + end + })) + return true + end + })) + end +end + +-- Todo : turn this into a lovely patch +G.FUNCS.end_consumeable = function(e, delayfac) + delayfac = delayfac or 1 + stop_use() + if G.booster_pack then + if G.booster_pack_sparkles then G.booster_pack_sparkles:fade(1*delayfac) end + if G.booster_pack_stars then G.booster_pack_stars:fade(1*delayfac) end + if G.booster_pack_meteors then G.booster_pack_meteors:fade(1*delayfac) end + G.booster_pack.alignment.offset.y = G.ROOM.T.y + 9 + + G.E_MANAGER:add_event(Event({trigger = 'after',delay = 0.2*delayfac,blocking = false, blockable = false, + func = function() + G.booster_pack:remove() + G.booster_pack = nil + return true + end})) + G.E_MANAGER:add_event(Event({trigger = 'after',delay = 1*delayfac,blocking = false, blockable = false, + func = function() + if G.booster_pack_sparkles then G.booster_pack_sparkles:remove(); G.booster_pack_sparkles = nil end + if G.booster_pack_stars then G.booster_pack_stars:remove(); G.booster_pack_stars = nil end + if G.booster_pack_meteors then G.booster_pack_meteors:remove(); G.booster_pack_meteors = nil end + return true + end})) + end + + delay(0.2*delayfac) + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 0.2*delayfac, + func = function() + G.FUNCS.draw_from_hand_to_deck() + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 0.2*delayfac, + func = function() + G.CONTROLLER.interrupt.focus = true + G.E_MANAGER:add_event(Event({func = function() + if G.shop then G.CONTROLLER:snap_to({node = G.shop:get_UIE_by_ID('next_round_button')}) end + return true end })) + if SMODS.GameStates[G.STATE] and SMODS.GameStates[G.STATE].exit_after_end_consumable then + SMODS.exit_state(nil, G.GAME.PACK_INTERRUPT) + elseif not SMODS.GameStates[G.STATE] then + G.STATE = G.GAME.PACK_INTERRUPT + end + ease_background_colour_blind(G.GAME.PACK_INTERRUPT) + G.GAME.PACK_INTERRUPT = nil + return true + end + })) + SMODS.calculate_context({ending_booster = true, booster = booster_obj}) + booster_obj = nil + for i = 1, #G.GAME.tags do + if G.GAME.tags[i]:apply_to_run({type = 'new_blind_choice'}) then break end + end + + G.E_MANAGER:add_event(Event({trigger = 'after',delay = 0.2*delayfac, + func = function() + save_run() + return true + end})) + + return true + end + })) +end \ No newline at end of file From 504e9044e49d171bfb1edc5d7e61517165e78bea Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Sat, 4 Apr 2026 21:24:40 +0200 Subject: [PATCH 08/39] Refactored `on_enter` and `on_exit` for `SMODS.GameState`s and changed `SMODS.STATE.BLIND` timing *Title --- lovely/booster.toml | 4 +- src/game_object.lua | 147 ++++++++++++++++++++++++++------------------ 2 files changed, 89 insertions(+), 62 deletions(-) diff --git a/lovely/booster.toml b/lovely/booster.toml index fcf7a8d8c..aa4099f89 100644 --- a/lovely/booster.toml +++ b/lovely/booster.toml @@ -34,7 +34,7 @@ payload = """ booster_obj = self.config.center if booster_obj and SMODS.Centers[booster_obj.key] then SMODS.OPENED_BOOSTER = self - SMODS.enter_state(SMODS.STATES.BOOSTER_OPENED, nil, true) + SMODS.enter_state(SMODS.STATES.BOOSTER_OPENED, nil, {from_hold = true}) end G.GAME.pack_choices = math.min((self.ability.choose or self.config.center.config.choose or 1) + (G.GAME.modifiers.booster_choice_mod or 0), self.ability.extra and math.max(1, self.ability.extra + (G.GAME.modifiers.booster_size_mod or 0)) or self.config.center.extra and math.max(1, self.config.center.extra + (G.GAME.modifiers.booster_size_mod or 0)) or 1) """ @@ -206,4 +206,4 @@ target = 'card.lua' match_indent = true position = 'after' pattern = '''if self.shop_voucher then G.GAME.current_round.voucher = nil end''' -payload = '''SMODS.enter_state(SMODS.STATES.REDEEM_VOUCHER, nil, true)''' +payload = '''SMODS.enter_state(SMODS.STATES.REDEEM_VOUCHER, nil, {from_hold = true})''' diff --git a/src/game_object.lua b/src/game_object.lua index 28f976965..d1a85ec5e 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -4168,39 +4168,54 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. if G.round_eval and not exempt_map[SMODS.STATES.ROUND_EVAL] then G.round_eval:remove(); G.round_eval = nil end end - function SMODS.enter_state(state, args, do_hold_state) + function SMODS.enter_state(state, enter_args, exit_args) local previous_state = SMODS.STATE or G.STATE -- It only handles on_exit() and on_enter() for SMODS.GameState states if previous_state == state then return end + enter_args = enter_args or {} + exit_args = exit_args or {} + exit_args.new_state = exit_args.new_state or state if SMODS.GameStates[previous_state] then - SMODS.GameStates[previous_state]:on_exit({new_state=state}, do_hold_state) - if not do_hold_state then + SMODS.GameStates[previous_state]:on_exit(exit_args) + if not exit_args.from_hold then SMODS.pop_from_state_stack(previous_state) end end G.STATE = state if SMODS.GameStates[state] then SMODS.STATE = state - SMODS.GameStates[state]:on_enter(args) - SMODS.push_to_state_stack(state, args) + SMODS.GameStates[state]:on_enter(enter_args) + SMODS.push_to_state_stack(state, enter_args) end end - function SMODS.exit_state(args, default_state_override) + function SMODS.exit_state(exit_args, enter_args, default) local current_state = SMODS.STATE or G.STATE + local new_state + if #SMODS.state_stack < 1 then + new_state = default.state_override or SMODS.default_state + else + new_state = SMODS.state_stack[#SMODS.state_stack].state + end + exit_args = exit_args or {} + exit_args.new_state = exit_args.new_state or new_state + enter_args = enter_args or {} + enter_args.from_hold = true + default = default or {} + default.enter_args = default.enter_args or {} if SMODS.GameStates[current_state] then - SMODS.GameStates[current_state]:on_exit(args) + SMODS.GameStates[current_state]:on_exit(exit_args) SMODS.pop_from_state_stack(current_state) end if #SMODS.state_stack < 1 then G.STATE = nil SMODS.STATE = nil - SMODS.enter_state(default_state_override or SMODS.default_state) + SMODS.enter_state(new_state, default.enter_args) return end - G.STATE = SMODS.state_stack[#SMODS.state_stack].state + G.STATE = new_state if SMODS.GameStates[G.STATE] then SMODS.STATE = G.STATE - SMODS.GameStates[G.STATE]:on_enter(args, true) + SMODS.GameStates[G.STATE]:on_enter(enter_args) end end @@ -4223,8 +4238,8 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. inject = function (self, i) end, - on_enter = function (self, args, from_hold) end, - on_exit = function (self, args, from_hold) end, + on_enter = function (self, args) end, + on_exit = function (self, args) end, update = function (self, dt) end, ease_background_colour = nil, -- function exit_after_use_card = false, -- Used for consumable states like SMODS.STATES.REDEEM_VOUCHER @@ -4246,9 +4261,10 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. SMODS.GameState { key = SMODS.STATES.SHOP, - on_enter = function (self, args, from_hold) - if from_hold then + on_enter = function (self, args) + if args.from_hold then -- Extracted from G.FUNCS.use_card() + -- Todo : Make better if G.shop then G.shop.alignment.offset.y = G.shop.alignment.offset.py G.shop.alignment.offset.py = nil @@ -4375,8 +4391,8 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. end })) end, - on_exit = function (self, args, from_hold) - if from_hold then + on_exit = function (self, args) + if args.from_hold then if G.shop and not G.shop.alignment.offset.py then G.shop.alignment.offset.py = G.shop.alignment.offset.y G.shop.alignment.offset.y = G.ROOM.T.y + 29 @@ -4428,8 +4444,8 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. SMODS.GameState { key = SMODS.STATES.ROUND_EVAL, - on_enter = function (self, args, from_hold) - if from_hold then + on_enter = function (self, args) + if args.from_hold then if G.round_eval then G.round_eval.alignment.offset.y = G.round_eval.alignment.offset.py G.round_eval.alignment.offset.py = nil @@ -4474,8 +4490,8 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. end })) end, - on_exit = function (self, args, from_hold) - if from_hold then + on_exit = function (self, args) + if args.from_hold then if G.round_eval and not G.round_eval.alignment.offset.py then G.round_eval.alignment.offset.py = G.round_eval.alignment.offset.y G.round_eval.alignment.offset.y = G.ROOM.T.y + 29 @@ -4527,7 +4543,11 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. SMODS.GameState { key = SMODS.STATES.BLIND, - on_enter = function (self, args, from_hold) + on_enter = function (self, args) + if args.from_hold then + -- Todo: Implement + return + end G.E_MANAGER:add_event(Event({ trigger = "immediate", func = function () @@ -4561,49 +4581,56 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. end })) end, - on_exit = function (self, args, from_hold) + on_exit = function (self, args) G.GAME.facing_blind = nil - if not from_hold then - -- Taken from G.FUNCS.evaluate_round(), defeats blind - -- The extra nested immediate events should hopefully preserve the vanilla timing - G.E_MANAGER:add_event(Event({ - trigger = "immediate", - func = function () - G.E_MANAGER:add_event(Event({ - trigger = "immediate", - func = function () - G.E_MANAGER:add_event(Event({ - trigger = "immediate", - blocking = false, - func = function () - if not G.round_eval or (G.round_eval.alignment.offset.y == -7.8 and math.abs(G.round_eval.T.y - G.round_eval.VT.y) < 3) then - G.E_MANAGER:add_event(Event({ - trigger = "immediate", - func = function () - G.E_MANAGER:add_event(Event({ - trigger = 'before', - delay = 1.3*math.min(G.GAME.blind.dollars+2, 7)/2*0.15 + 0.5, - func = function() - G.GAME.blind:defeat() - return true - end - })) - delay(0.2) - return true - end - })) + if args.no_defeat then -- Example: SMODS.enter_state(SMODS.STATES.SHOP, nil, {no_defeat = true}) -> This opens the shop without defeating the blind or holding SMODS.STATES.BLIND + -- Todo: implement this + return + end + if args.from_hold then + -- Todo: implement holding SMODS.STATES.BLIND + return + end + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + blocking = false, + func = function () + if args.new_state == SMODS.STATES.ROUND_EVAL then -- Precise vanilla timing + if G.round_eval and G.round_eval.alignment.offset.y == -7.8 and math.abs(G.round_eval.T.y - G.round_eval.VT.y) < 3 then + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + func = function () + G.E_MANAGER:add_event(Event({ + trigger = 'before', + delay = 1.3*math.min(G.GAME.blind.dollars+2, 7)/2*0.15 + 0.5, + func = function() + G.GAME.blind:defeat() return true end - end, - })) + })) + delay(0.2) + return true + end + })) + return true + end + else + G.FUNCS.draw_from_hand_to_discard() + G.FUNCS.draw_from_discard_to_deck() + G.E_MANAGER:add_event(Event({ + trigger = "after", + blockable = false, + delay = 0.7, + func = function () + G.GAME.blind:defeat() return true end })) + delay(0.4) return true end - })) - end - + end + })) end, ease_background_colour = function (self, blind_override) local blindname = ((blind_override or (G.GAME.blind and G.GAME.blind.name ~= '' and G.GAME.blind.name)) or 'Small Blind') @@ -4625,8 +4652,8 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. SMODS.GameState { key = SMODS.STATES.BLIND_SELECT, - on_enter = function (self, args, from_hold) - if from_hold then + on_enter = function (self, args) + if args.from_hold then if G.blind_select then G.blind_select.alignment.offset.y = G.blind_select.alignment.offset.py G.blind_select.alignment.offset.py = nil @@ -4669,8 +4696,8 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. end })) end, - on_exit = function (self, args, from_hold) - if from_hold then + on_exit = function (self, args) + if args.from_hold then if G.blind_select and not G.blind_select.alignment.offset.py then G.blind_select.alignment.offset.py = G.blind_select.alignment.offset.y G.blind_select.alignment.offset.y = G.ROOM.T.y + 39 From 86aaa7b72bdc8f54bd65ff1eeac7b579367c303f Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Sat, 4 Apr 2026 22:34:26 +0200 Subject: [PATCH 09/39] More refactoring + WIP Per-instance state stuff (e.g. to allow creating and returning to multiple different Shops) *Title --- src/game_object.lua | 43 ++++++++++++++++++++++++------------------- src/overrides.lua | 4 ++-- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/game_object.lua b/src/game_object.lua index d1a85ec5e..e97025ee8 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -4146,7 +4146,8 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. end function SMODS.push_to_state_stack(state, args) - table.insert(SMODS.state_stack, {state=state, args=args}) + local data = SMODS.GameStates[state] and SMODS.GameStates[state].store_to_stack() + table.insert(SMODS.state_stack, {state=state, args=args, data=data}) end function SMODS.pop_from_state_stack(state) @@ -4162,39 +4163,43 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. function SMODS.clear_states(exempt_map) exempt_map = exempt_map or {} + for i, state_table in ipairs(SMODS.state_stack) do + exempt_map[state_table.state] = true + end if G.blind_select and not exempt_map[SMODS.STATES.BLIND_SELECT] then G.blind_select:remove(); G.blind_select = nil end if G.shop and not exempt_map[SMODS.STATES.SHOP] then G.shop:remove(); G.shop = nil end if G.buttons and not exempt_map[SMODS.STATES.BLIND] then G.buttons:remove(); G.buttons = nil end if G.round_eval and not exempt_map[SMODS.STATES.ROUND_EVAL] then G.round_eval:remove(); G.round_eval = nil end end - function SMODS.enter_state(state, enter_args, exit_args) - local previous_state = SMODS.STATE or G.STATE -- It only handles on_exit() and on_enter() for SMODS.GameState states - if previous_state == state then return end + function SMODS.enter_state(new_state, enter_args, exit_args) + local current_state = SMODS.STATE or G.STATE -- It only handles on_exit() and on_enter() for SMODS.GameState states + if current_state == new_state then return end enter_args = enter_args or {} exit_args = exit_args or {} - exit_args.new_state = exit_args.new_state or state - if SMODS.GameStates[previous_state] then - SMODS.GameStates[previous_state]:on_exit(exit_args) + exit_args.new_state = exit_args.new_state or new_state + SMODS.clear_states() + if SMODS.GameStates[current_state] then + SMODS.GameStates[current_state]:on_exit(exit_args) if not exit_args.from_hold then - SMODS.pop_from_state_stack(previous_state) + SMODS.pop_from_state_stack(current_state) end end - G.STATE = state - if SMODS.GameStates[state] then - SMODS.STATE = state - SMODS.GameStates[state]:on_enter(enter_args) - SMODS.push_to_state_stack(state, enter_args) + G.STATE = new_state + if SMODS.GameStates[new_state] then + SMODS.STATE = new_state + SMODS.push_to_state_stack(new_state, enter_args) + SMODS.GameStates[new_state]:on_enter(enter_args) end end function SMODS.exit_state(exit_args, enter_args, default) local current_state = SMODS.STATE or G.STATE local new_state - if #SMODS.state_stack < 1 then + if #SMODS.state_stack < 2 then new_state = default.state_override or SMODS.default_state else - new_state = SMODS.state_stack[#SMODS.state_stack].state + new_state = SMODS.state_stack[#SMODS.state_stack - 1].state end exit_args = exit_args or {} exit_args.new_state = exit_args.new_state or new_state @@ -4202,6 +4207,7 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. enter_args.from_hold = true default = default or {} default.enter_args = default.enter_args or {} + SMODS.clear_states() if SMODS.GameStates[current_state] then SMODS.GameStates[current_state]:on_exit(exit_args) SMODS.pop_from_state_stack(current_state) @@ -4241,6 +4247,7 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. on_enter = function (self, args) end, on_exit = function (self, args) end, update = function (self, dt) end, + store_to_stack = function () end, -- Called when SMODS.push_to_state_stack() is called for this GameState, allows returning thus storing per-instance state data, to restore when e.g. re-entering from being held. ease_background_colour = nil, -- function exit_after_use_card = false, -- Used for consumable states like SMODS.STATES.REDEEM_VOUCHER exit_after_end_consumable = false, -- Used for booster-like states like SMODS.STATES.BOOSTER_OPENED @@ -4268,13 +4275,13 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. if G.shop then G.shop.alignment.offset.y = G.shop.alignment.offset.py G.shop.alignment.offset.py = nil + G.SHOP_SIGN.alignment.offset.y = 0 end return end G.E_MANAGER:add_event(Event({ trigger = "immediate", func = function () - SMODS.clear_states() stop_use() G.STATE_COMPLETE = true ease_background_colour_blind(G.STATES.SHOP) @@ -4396,6 +4403,7 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. if G.shop and not G.shop.alignment.offset.py then G.shop.alignment.offset.py = G.shop.alignment.offset.y G.shop.alignment.offset.y = G.ROOM.T.y + 29 + G.SHOP_SIGN.alignment.offset.y = -15 end return end @@ -4455,7 +4463,6 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. G.E_MANAGER:add_event(Event({ trigger = "immediate", func = function () - SMODS.clear_states() stop_use() G.STATE_COMPLETE = true G.E_MANAGER:add_event(Event({ @@ -4551,7 +4558,6 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. G.E_MANAGER:add_event(Event({ trigger = "immediate", func = function () - SMODS.clear_states() stop_use() G.GAME.facing_blind = true @@ -4663,7 +4669,6 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. G.E_MANAGER:add_event(Event({ trigger = "immediate", func = function() - SMODS.clear_states() stop_use() ease_background_colour_blind(SMODS.STATES.BLIND_SELECT) G.E_MANAGER:add_event(Event({ func = function() save_run(); return true end})) diff --git a/src/overrides.lua b/src/overrides.lua index 60538fe42..d86c8f6e5 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -3156,7 +3156,7 @@ G.FUNCS.use_card = function(e, mute, nosave) delay = 0.1, func = function() if SMODS.GameStates[G.STATE] and SMODS.GameStates[G.STATE].exit_after_use_card then -- SMODS.STATES.BOOSTER_OPENED is handled by G.FUNCS.end_consumeable() below - SMODS.exit_state(nil, prev_state) + SMODS.exit_state(nil, nil, {state_override=prev_state}) elseif not SMODS.GameStates[G.STATE] then G.STATE = prev_state end @@ -3253,7 +3253,7 @@ G.FUNCS.end_consumeable = function(e, delayfac) if G.shop then G.CONTROLLER:snap_to({node = G.shop:get_UIE_by_ID('next_round_button')}) end return true end })) if SMODS.GameStates[G.STATE] and SMODS.GameStates[G.STATE].exit_after_end_consumable then - SMODS.exit_state(nil, G.GAME.PACK_INTERRUPT) + SMODS.exit_state(nil, nil, {state_override=G.GAME.PACK_INTERRUPT}) elseif not SMODS.GameStates[G.STATE] then G.STATE = G.GAME.PACK_INTERRUPT end From 75a73840884216d865be225a23e0af9a4030d6f7 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Sat, 4 Apr 2026 22:37:30 +0200 Subject: [PATCH 10/39] Moved `SMODS.GameStates` to new file and removed `SMODS.clear_states()` */-Title, `SMODS.clear_states()` was kinda unnecessary, as states should clean up after themselves anyway. --- src/game_object.lua | 611 ------------------------------- src/game_objects/game_states.lua | 597 ++++++++++++++++++++++++++++++ 2 files changed, 597 insertions(+), 611 deletions(-) create mode 100644 src/game_objects/game_states.lua diff --git a/src/game_object.lua b/src/game_object.lua index e97025ee8..176442c01 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -4125,617 +4125,6 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. text = '^' } - ------------------------------------------------------------------------------------------------- - ----- API CODE SMODS.GameState - ------------------------------------------------------------------------------------------------- - - SMODS.STATES = { - BOOSTER_OPENED = "BOOSTER_OPENED", - REDEEM_VOUCHER = "REDEEM_VOUCHER", - SHOP = "SHOP", - ROUND_EVAL = "ROUND_EVAL", - BLIND = "BLIND", - BLIND_SELECT = "BLIND_SELECT" - } - SMODS.default_state = SMODS.STATES.BLIND_SELECT - - SMODS.state_stack = {} - - function SMODS.get_current_state() - return #SMODS.state_stack > 0 and SMODS.state_stack[#SMODS.state_stack] - end - - function SMODS.push_to_state_stack(state, args) - local data = SMODS.GameStates[state] and SMODS.GameStates[state].store_to_stack() - table.insert(SMODS.state_stack, {state=state, args=args, data=data}) - end - - function SMODS.pop_from_state_stack(state) - if #SMODS.state_stack < 1 then return end - if SMODS.state_stack[#SMODS.state_stack].state == state then - table.remove(SMODS.state_stack, #SMODS.state_stack) - end - end - - function SMODS.clear_state_stack() - SMODS.state_stack = {} - end - - function SMODS.clear_states(exempt_map) - exempt_map = exempt_map or {} - for i, state_table in ipairs(SMODS.state_stack) do - exempt_map[state_table.state] = true - end - if G.blind_select and not exempt_map[SMODS.STATES.BLIND_SELECT] then G.blind_select:remove(); G.blind_select = nil end - if G.shop and not exempt_map[SMODS.STATES.SHOP] then G.shop:remove(); G.shop = nil end - if G.buttons and not exempt_map[SMODS.STATES.BLIND] then G.buttons:remove(); G.buttons = nil end - if G.round_eval and not exempt_map[SMODS.STATES.ROUND_EVAL] then G.round_eval:remove(); G.round_eval = nil end - end - - function SMODS.enter_state(new_state, enter_args, exit_args) - local current_state = SMODS.STATE or G.STATE -- It only handles on_exit() and on_enter() for SMODS.GameState states - if current_state == new_state then return end - enter_args = enter_args or {} - exit_args = exit_args or {} - exit_args.new_state = exit_args.new_state or new_state - SMODS.clear_states() - if SMODS.GameStates[current_state] then - SMODS.GameStates[current_state]:on_exit(exit_args) - if not exit_args.from_hold then - SMODS.pop_from_state_stack(current_state) - end - end - G.STATE = new_state - if SMODS.GameStates[new_state] then - SMODS.STATE = new_state - SMODS.push_to_state_stack(new_state, enter_args) - SMODS.GameStates[new_state]:on_enter(enter_args) - end - end - - function SMODS.exit_state(exit_args, enter_args, default) - local current_state = SMODS.STATE or G.STATE - local new_state - if #SMODS.state_stack < 2 then - new_state = default.state_override or SMODS.default_state - else - new_state = SMODS.state_stack[#SMODS.state_stack - 1].state - end - exit_args = exit_args or {} - exit_args.new_state = exit_args.new_state or new_state - enter_args = enter_args or {} - enter_args.from_hold = true - default = default or {} - default.enter_args = default.enter_args or {} - SMODS.clear_states() - if SMODS.GameStates[current_state] then - SMODS.GameStates[current_state]:on_exit(exit_args) - SMODS.pop_from_state_stack(current_state) - end - if #SMODS.state_stack < 1 then - G.STATE = nil - SMODS.STATE = nil - SMODS.enter_state(new_state, default.enter_args) - return - end - G.STATE = new_state - if SMODS.GameStates[G.STATE] then - SMODS.STATE = G.STATE - SMODS.GameStates[G.STATE]:on_enter(enter_args) - end - end - - local delete_run_ref = Game.delete_run - function Game:delete_run() - local ret = delete_run_ref(self) - SMODS.STATE = nil - SMODS.clear_state_stack() - return ret - end - - SMODS.GameStates = {} - SMODS.GameState = SMODS.GameObject:extend{ - set = 'GameState', - obj_table = SMODS.GameStates, - obj_buffer = {}, - required_parameters = { - 'key', - }, - inject = function (self, i) - - end, - on_enter = function (self, args) end, - on_exit = function (self, args) end, - update = function (self, dt) end, - store_to_stack = function () end, -- Called when SMODS.push_to_state_stack() is called for this GameState, allows returning thus storing per-instance state data, to restore when e.g. re-entering from being held. - ease_background_colour = nil, -- function - exit_after_use_card = false, -- Used for consumable states like SMODS.STATES.REDEEM_VOUCHER - exit_after_end_consumable = false, -- Used for booster-like states like SMODS.STATES.BOOSTER_OPENED - } - - SMODS.GameState { - key = SMODS.STATES.BOOSTER_OPENED, - update = function (self, dt) - SMODS.OPENED_BOOSTER.config.center:update_pack(dt) - end, - exit_after_end_consumable = true, - } - - SMODS.GameState { - key = SMODS.STATES.REDEEM_VOUCHER, - exit_after_use_card = true, - } - - SMODS.GameState { - key = SMODS.STATES.SHOP, - on_enter = function (self, args) - if args.from_hold then - -- Extracted from G.FUNCS.use_card() - -- Todo : Make better - if G.shop then - G.shop.alignment.offset.y = G.shop.alignment.offset.py - G.shop.alignment.offset.py = nil - G.SHOP_SIGN.alignment.offset.y = 0 - end - return - end - G.E_MANAGER:add_event(Event({ - trigger = "immediate", - func = function () - stop_use() - G.STATE_COMPLETE = true - ease_background_colour_blind(G.STATES.SHOP) - local shop_exists = not not G.shop - G.shop = G.shop or UIBox{ - definition = G.UIDEF.shop(), - config = {align='tmi', offset = {x=0,y=G.ROOM.T.y+11},major = G.hand, bond = 'Weak'} - } - -- Moved here from G.FUNCS.cash_out() - G.GAME.current_round.jokers_purchased = 0 - G.GAME.shop_free = nil - G.GAME.shop_d6ed = nil - ------- - G.E_MANAGER:add_event(Event({ - func = function() - G.shop.alignment.offset.y = -5.3 - G.shop.alignment.offset.x = 0 - G.E_MANAGER:add_event(Event({ - trigger = 'after', - delay = 0.2, - blockable = false, - func = function() - if math.abs(G.shop.T.y - G.shop.VT.y) < 3 then - G.ROOM.jiggle = G.ROOM.jiggle + 3 - play_sound('cardFan2') - for i = 1, #G.GAME.tags do - G.GAME.tags[i]:apply_to_run({type = 'shop_start'}) - end - local nosave_shop = nil - if not shop_exists then - if G.load_shop_jokers then - nosave_shop = true - G.shop_jokers:load(G.load_shop_jokers) - for k, v in ipairs(G.shop_jokers.cards) do - create_shop_card_ui(v) - if v.ability.consumeable then v:start_materialize() end - for _kk, vvv in ipairs(G.GAME.tags) do - if vvv:apply_to_run({type = 'store_joker_modify', card = v}) then break end - end - end - G.load_shop_jokers = nil - else - for i = 1, G.GAME.shop.joker_max - #G.shop_jokers.cards do - G.shop_jokers:emplace(create_card_for_shop(G.shop_jokers)) - end - end - - if G.load_shop_vouchers then - nosave_shop = true - G.shop_vouchers:load(G.load_shop_vouchers) - for k, v in ipairs(G.shop_vouchers.cards) do - create_shop_card_ui(v) - v:start_materialize() - end - G.load_shop_vouchers = nil - else - local vouchers_to_spawn = 0 - for _,_ in pairs(G.GAME.current_round.voucher.spawn) do vouchers_to_spawn = vouchers_to_spawn + 1 end - if vouchers_to_spawn < G.GAME.starting_params.vouchers_in_shop + (G.GAME.modifiers.extra_vouchers or 0) then - SMODS.get_next_vouchers(G.GAME.current_round.voucher) - end - for _, key in ipairs(G.GAME.current_round.voucher or {}) do - if G.P_CENTERS[key] and G.GAME.current_round.voucher.spawn[key] then - SMODS.add_voucher_to_shop(key) - end - end - end - - if G.load_shop_booster then - nosave_shop = true - G.shop_booster:load(G.load_shop_booster) - for k, v in ipairs(G.shop_booster.cards) do - create_shop_card_ui(v) - v:start_materialize() - end - G.load_shop_booster = nil - else - for i=1, G.GAME.starting_params.boosters_in_shop + (G.GAME.modifiers.extra_boosters or 0) do - G.GAME.current_round.used_packs = G.GAME.current_round.used_packs or {} - if not G.GAME.current_round.used_packs[i] then - G.GAME.current_round.used_packs[i] = get_pack('shop_pack').key - end - - if G.GAME.current_round.used_packs[i] ~= 'USED' then - local card = Card(G.shop_booster.T.x + G.shop_booster.T.w/2, - G.shop_booster.T.y, G.CARD_W*1.27, G.CARD_H*1.27, G.P_CARDS.empty, G.P_CENTERS[G.GAME.current_round.used_packs[i]], {bypass_discovery_center = true, bypass_discovery_ui = true}) - create_shop_card_ui(card, 'Booster', G.shop_booster) - card.ability.booster_pos = i - card:start_materialize() - G.shop_booster:emplace(card) - end - end - - for i = 1, #G.GAME.tags do - G.GAME.tags[i]:apply_to_run({type = 'voucher_add'}) - end - for i = 1, #G.GAME.tags do - G.GAME.tags[i]:apply_to_run({type = 'shop_final_pass'}) - end - end - end - - if not nosave_shop then SMODS.calculate_context({starting_shop = true}) end - G.CONTROLLER:snap_to({node = G.shop:get_UIE_by_ID('next_round_button')}) - if not nosave_shop then G.E_MANAGER:add_event(Event({ func = function() save_run(); return true end})) end - return true - end - end - })) - return true - end - })) - return true - end - })) - end, - on_exit = function (self, args) - if args.from_hold then - if G.shop and not G.shop.alignment.offset.py then - G.shop.alignment.offset.py = G.shop.alignment.offset.y - G.shop.alignment.offset.y = G.ROOM.T.y + 29 - G.SHOP_SIGN.alignment.offset.y = -15 - end - return - end - stop_use() - G.CONTROLLER.locks.toggle_shop = true - if G.shop then - if not from_hold then - SMODS.calculate_context({ending_shop = true}) - end - G.E_MANAGER:add_event(Event({ - trigger = 'immediate', - func = function() - G.shop.alignment.offset.y = G.ROOM.T.y + 29 - G.SHOP_SIGN.alignment.offset.y = -15 - return true - end - })) - if from_hold then - G.E_MANAGER:add_event(Event({ - trigger = 'after', - delay = 0.5, - func = function () - G.CONTROLLER.locks.toggle_shop = nil - return true - end - })) - return - end - G.E_MANAGER:add_event(Event({ - trigger = 'after', - delay = 0.5, - func = function() - G.shop:remove() - G.shop = nil - G.SHOP_SIGN:remove() - G.SHOP_SIGN = nil - G.STATE_COMPLETE = false - G.CONTROLLER.locks.toggle_shop = nil - return true - end - })) - end - end, - check_win = true, - } - - SMODS.GameState { - key = SMODS.STATES.ROUND_EVAL, - on_enter = function (self, args) - if args.from_hold then - if G.round_eval then - G.round_eval.alignment.offset.y = G.round_eval.alignment.offset.py - G.round_eval.alignment.offset.py = nil - end - return - end - G.E_MANAGER:add_event(Event({ - trigger = "immediate", - func = function () - stop_use() - G.STATE_COMPLETE = true - G.E_MANAGER:add_event(Event({ - trigger = 'immediate', - func = function() - save_run() - ease_background_colour_blind(G.STATES.ROUND_EVAL) - G.round_eval = UIBox{ - definition = create_UIBox_round_evaluation(), - config = {align="bm", offset = {x=0,y=G.ROOM.T.y + 19},major = G.hand, bond = 'Weak'} - } - G.round_eval.alignment.offset.x = 0 - G.E_MANAGER:add_event(Event({ - trigger = 'immediate', - func = function() - if G.round_eval.alignment.offset.y ~= -7.8 then - G.round_eval.alignment.offset.y = -7.8 - else - if math.abs(G.round_eval.T.y - G.round_eval.VT.y) < 3 then - G.ROOM.jiggle = G.ROOM.jiggle + 3 - play_sound('cardFan2') - delay(0.1) - G.FUNCS.evaluate_round() - return true - end - end - end})) - return true - end - })) - return true - end - })) - end, - on_exit = function (self, args) - if args.from_hold then - if G.round_eval and not G.round_eval.alignment.offset.py then - G.round_eval.alignment.offset.py = G.round_eval.alignment.offset.y - G.round_eval.alignment.offset.y = G.ROOM.T.y + 29 - end - return - end - stop_use() - if G.round_eval then - G.round_eval.alignment.offset.y = G.ROOM.T.y + 15 - G.round_eval.alignment.offset.x = 0 - G.deck:shuffle('cashout'..G.GAME.round_resets.ante) - G.deck:hard_set_T() - delay(0.3) - G.GAME.current_round.discards_left = math.max(0, G.GAME.round_resets.discards + G.GAME.round_bonus.discards) - G.GAME.current_round.hands_left = (math.max(1, G.GAME.round_resets.hands + G.GAME.round_bonus.next_hands)) - G.E_MANAGER:add_event(Event({ - trigger = 'immediate', - func = function() - if G.round_eval then - G.round_eval:remove() - G.round_eval = nil - end - -- G.STATE_COMPLETE = false - return true - end - })) - ease_dollars(G.GAME.current_round.dollars) - G.E_MANAGER:add_event(Event({ - func = function() - G.GAME.previous_round.dollars = G.GAME.dollars - return true - end - })) - play_sound("coin7") - G.VIBRATION = G.VIBRATION + 1 - end - - ease_chips(0) - if G.GAME.round_resets.blind_states.Boss == 'Defeated' then - G.GAME.round_resets.blind_ante = G.GAME.round_resets.ante - G.GAME.round_resets.blind_tags.Small = get_next_tag_key() - G.GAME.round_resets.blind_tags.Big = get_next_tag_key() - end - reset_blinds() - delay(0.6) - end, - check_win = true, - } - - SMODS.GameState { - key = SMODS.STATES.BLIND, - on_enter = function (self, args) - if args.from_hold then - -- Todo: Implement - return - end - G.E_MANAGER:add_event(Event({ - trigger = "immediate", - func = function () - stop_use() - G.GAME.facing_blind = true - - G.E_MANAGER:add_event(Event({ - trigger = 'immediate', - func = function() - ease_round(1) - inc_career_stat('c_rounds', 1) - if _DEMO then - G.SETTINGS.DEMO_ROUNDS = (G.SETTINGS.DEMO_ROUNDS or 0) + 1 - inc_steam_stat('demo_rounds') - G:save_settings() - end - G.GAME.round_resets.blind = G.P_BLINDS[args.key] - G.GAME.round_resets.blind_states[G.GAME.blind_on_deck] = 'Current' -- TODO : Check this / G.GAME.blind_on_deck - delay(0.2) - return true - end})) - G.E_MANAGER:add_event(Event({ - trigger = 'immediate', - func = function() - new_round() - return true - end - })) - return true - end - })) - end, - on_exit = function (self, args) - G.GAME.facing_blind = nil - if args.no_defeat then -- Example: SMODS.enter_state(SMODS.STATES.SHOP, nil, {no_defeat = true}) -> This opens the shop without defeating the blind or holding SMODS.STATES.BLIND - -- Todo: implement this - return - end - if args.from_hold then - -- Todo: implement holding SMODS.STATES.BLIND - return - end - G.E_MANAGER:add_event(Event({ - trigger = "immediate", - blocking = false, - func = function () - if args.new_state == SMODS.STATES.ROUND_EVAL then -- Precise vanilla timing - if G.round_eval and G.round_eval.alignment.offset.y == -7.8 and math.abs(G.round_eval.T.y - G.round_eval.VT.y) < 3 then - G.E_MANAGER:add_event(Event({ - trigger = "immediate", - func = function () - G.E_MANAGER:add_event(Event({ - trigger = 'before', - delay = 1.3*math.min(G.GAME.blind.dollars+2, 7)/2*0.15 + 0.5, - func = function() - G.GAME.blind:defeat() - return true - end - })) - delay(0.2) - return true - end - })) - return true - end - else - G.FUNCS.draw_from_hand_to_discard() - G.FUNCS.draw_from_discard_to_deck() - G.E_MANAGER:add_event(Event({ - trigger = "after", - blockable = false, - delay = 0.7, - func = function () - G.GAME.blind:defeat() - return true - end - })) - delay(0.4) - return true - end - end - })) - end, - ease_background_colour = function (self, blind_override) - local blindname = ((blind_override or (G.GAME.blind and G.GAME.blind.name ~= '' and G.GAME.blind.name)) or 'Small Blind') - blindname = (blindname == '' and 'Small Blind' or blindname) - - local boss_col = G.C.BLACK - for k, v in pairs(G.P_BLINDS) do - if v.name == blindname then - if v.boss and v.boss.showdown or v.blind_types and v.blind_types.Showdown then - ease_background_colour{new_colour = G.C.BLUE, special_colour = G.C.RED, tertiary_colour = darken(G.C.BLACK, 0.4), contrast = 3} - return - end - boss_col = v.boss_colour or G.C.BLACK - end - end - ease_background_colour{new_colour = lighten(mix_colours(boss_col, G.C.BLACK, 0.3), 0.1), special_colour = boss_col, contrast = 2} - end - } - - SMODS.GameState { - key = SMODS.STATES.BLIND_SELECT, - on_enter = function (self, args) - if args.from_hold then - if G.blind_select then - G.blind_select.alignment.offset.y = G.blind_select.alignment.offset.py - G.blind_select.alignment.offset.py = nil - end - return - end - G.E_MANAGER:add_event(Event({ - trigger = "immediate", - func = function() - stop_use() - ease_background_colour_blind(SMODS.STATES.BLIND_SELECT) - G.E_MANAGER:add_event(Event({ func = function() save_run(); return true end})) - G.CONTROLLER.interrupt.focus = true - G.E_MANAGER:add_event(Event({ func = function() - G.E_MANAGER:add_event(Event({ - trigger = 'immediate', - func = function() - play_sound('cancel') - G.blind_select = UIBox{ - definition = create_UIBox_blind_select(), - config = {align="bmi", offset = {x=0,y=G.ROOM.T.y + 29},major = G.hand, bond = 'Weak'} - } - G.blind_select.alignment.offset.y = 0.8-(G.hand.T.y - G.jokers.T.y) + G.blind_select.T.h - G.ROOM.jiggle = G.ROOM.jiggle + 3 - G.blind_select.alignment.offset.x = 0 - G.CONTROLLER.lock_input = false - for i = 1, #G.GAME.tags do - G.GAME.tags[i]:apply_to_run({type = 'immediate'}) - end - for i = 1, #G.GAME.tags do - if G.GAME.tags[i]:apply_to_run({type = 'new_blind_choice'}) then break end - end - return true - end - })) - return true - end})) - return true - end - })) - end, - on_exit = function (self, args) - if args.from_hold then - if G.blind_select and not G.blind_select.alignment.offset.py then - G.blind_select.alignment.offset.py = G.blind_select.alignment.offset.y - G.blind_select.alignment.offset.y = G.ROOM.T.y + 39 - end - return - end - G.blind_prompt_box:get_UIE_by_ID('prompt_dynatext1').config.object.pop_delay = 0 - G.blind_prompt_box:get_UIE_by_ID('prompt_dynatext1').config.object:pop_out(5) - G.blind_prompt_box:get_UIE_by_ID('prompt_dynatext2').config.object.pop_delay = 0 - G.blind_prompt_box:get_UIE_by_ID('prompt_dynatext2').config.object:pop_out(5) - - G.E_MANAGER:add_event(Event({ - trigger = 'before', delay = 0.2, - func = function() - G.blind_prompt_box.alignment.offset.y = -10 - G.blind_select.alignment.offset.y = 40 - G.blind_select.alignment.offset.x = 0 - return true - end})) - G.E_MANAGER:add_event(Event({ - trigger = "immediate", - func = function () - G.blind_select:remove() - G.blind_prompt_box:remove() - G.blind_select = nil - return true - end - })) - - end, - check_win = true, - } - ------------------------------------------------------------------------------------------------- ----- API IMPORT GameObject.DrawStep ------------------------------------------------------------------------------------------------- diff --git a/src/game_objects/game_states.lua b/src/game_objects/game_states.lua new file mode 100644 index 000000000..b4cea85af --- /dev/null +++ b/src/game_objects/game_states.lua @@ -0,0 +1,597 @@ + ------------------------------------------------------------------------------------------------- + ----- API CODE SMODS.GameState + ------------------------------------------------------------------------------------------------- + + SMODS.STATES = { + BOOSTER_OPENED = "BOOSTER_OPENED", + REDEEM_VOUCHER = "REDEEM_VOUCHER", + SHOP = "SHOP", + ROUND_EVAL = "ROUND_EVAL", + BLIND = "BLIND", + BLIND_SELECT = "BLIND_SELECT" + } + SMODS.default_state = SMODS.STATES.BLIND_SELECT + + SMODS.state_stack = {} + + function SMODS.get_current_state() + return #SMODS.state_stack > 0 and SMODS.state_stack[#SMODS.state_stack] + end + + function SMODS.push_to_state_stack(state, args) + local data = SMODS.GameStates[state] and SMODS.GameStates[state].store_to_stack() + table.insert(SMODS.state_stack, {state=state, args=args, data=data}) + end + + function SMODS.pop_from_state_stack(state) + if #SMODS.state_stack < 1 then return end + if SMODS.state_stack[#SMODS.state_stack].state == state then + table.remove(SMODS.state_stack, #SMODS.state_stack) + end + end + + function SMODS.clear_state_stack() + SMODS.state_stack = {} + end + + function SMODS.enter_state(new_state, enter_args, exit_args) + local current_state = SMODS.STATE or G.STATE -- It only handles on_exit() and on_enter() for SMODS.GameState states + if current_state == new_state then return end + enter_args = enter_args or {} + exit_args = exit_args or {} + exit_args.new_state = exit_args.new_state or new_state + if SMODS.GameStates[current_state] then + SMODS.GameStates[current_state]:on_exit(exit_args) + if not exit_args.from_hold then + SMODS.pop_from_state_stack(current_state) + end + end + G.STATE = new_state + if SMODS.GameStates[new_state] then + SMODS.STATE = new_state + SMODS.push_to_state_stack(new_state, enter_args) + SMODS.GameStates[new_state]:on_enter(enter_args) + end + end + + function SMODS.exit_state(exit_args, enter_args, default) + local current_state = SMODS.STATE or G.STATE + local new_state + if #SMODS.state_stack < 2 then + new_state = default.state_override or SMODS.default_state + else + new_state = SMODS.state_stack[#SMODS.state_stack - 1].state + end + exit_args = exit_args or {} + exit_args.new_state = exit_args.new_state or new_state + enter_args = enter_args or {} + enter_args.from_hold = true + default = default or {} + default.enter_args = default.enter_args or {} + if SMODS.GameStates[current_state] then + SMODS.GameStates[current_state]:on_exit(exit_args) + SMODS.pop_from_state_stack(current_state) + end + if #SMODS.state_stack < 1 then + G.STATE = nil + SMODS.STATE = nil + SMODS.enter_state(new_state, default.enter_args) + return + end + G.STATE = new_state + if SMODS.GameStates[G.STATE] then + SMODS.STATE = G.STATE + SMODS.GameStates[G.STATE]:on_enter(enter_args) + end + end + + local delete_run_ref = Game.delete_run + function Game:delete_run() + local ret = delete_run_ref(self) + SMODS.STATE = nil + SMODS.clear_state_stack() + return ret + end + + SMODS.GameStates = {} + SMODS.GameState = SMODS.GameObject:extend{ + set = 'GameState', + obj_table = SMODS.GameStates, + obj_buffer = {}, + required_parameters = { + 'key', + }, + inject = function (self, i) + + end, + on_enter = function (self, args) end, + on_exit = function (self, args) end, + update = function (self, dt) end, + store_to_stack = function () end, -- Called when SMODS.push_to_state_stack() is called for this GameState, allows returning thus storing per-instance state data, to restore when e.g. re-entering from being held. + ease_background_colour = nil, -- function + exit_after_use_card = false, -- Used for consumable states like SMODS.STATES.REDEEM_VOUCHER + exit_after_end_consumable = false, -- Used for booster-like states like SMODS.STATES.BOOSTER_OPENED + } + + SMODS.GameState { + key = SMODS.STATES.BOOSTER_OPENED, + update = function (self, dt) + SMODS.OPENED_BOOSTER.config.center:update_pack(dt) + end, + exit_after_end_consumable = true, + } + + SMODS.GameState { + key = SMODS.STATES.REDEEM_VOUCHER, + exit_after_use_card = true, + } + + SMODS.GameState { + key = SMODS.STATES.SHOP, + on_enter = function (self, args) + if args.from_hold then + -- Extracted from G.FUNCS.use_card() + -- Todo : Make better + if G.shop then + G.shop.alignment.offset.y = G.shop.alignment.offset.py + G.shop.alignment.offset.py = nil + G.SHOP_SIGN.alignment.offset.y = 0 + end + return + end + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + func = function () + stop_use() + G.STATE_COMPLETE = true + ease_background_colour_blind(G.STATES.SHOP) + local shop_exists = not not G.shop + G.shop = G.shop or UIBox{ + definition = G.UIDEF.shop(), + config = {align='tmi', offset = {x=0,y=G.ROOM.T.y+11},major = G.hand, bond = 'Weak'} + } + -- Moved here from G.FUNCS.cash_out() + G.GAME.current_round.jokers_purchased = 0 + G.GAME.shop_free = nil + G.GAME.shop_d6ed = nil + ------- + G.E_MANAGER:add_event(Event({ + func = function() + G.shop.alignment.offset.y = -5.3 + G.shop.alignment.offset.x = 0 + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 0.2, + blockable = false, + func = function() + if math.abs(G.shop.T.y - G.shop.VT.y) < 3 then + G.ROOM.jiggle = G.ROOM.jiggle + 3 + play_sound('cardFan2') + for i = 1, #G.GAME.tags do + G.GAME.tags[i]:apply_to_run({type = 'shop_start'}) + end + local nosave_shop = nil + if not shop_exists then + if G.load_shop_jokers then + nosave_shop = true + G.shop_jokers:load(G.load_shop_jokers) + for k, v in ipairs(G.shop_jokers.cards) do + create_shop_card_ui(v) + if v.ability.consumeable then v:start_materialize() end + for _kk, vvv in ipairs(G.GAME.tags) do + if vvv:apply_to_run({type = 'store_joker_modify', card = v}) then break end + end + end + G.load_shop_jokers = nil + else + for i = 1, G.GAME.shop.joker_max - #G.shop_jokers.cards do + G.shop_jokers:emplace(create_card_for_shop(G.shop_jokers)) + end + end + + if G.load_shop_vouchers then + nosave_shop = true + G.shop_vouchers:load(G.load_shop_vouchers) + for k, v in ipairs(G.shop_vouchers.cards) do + create_shop_card_ui(v) + v:start_materialize() + end + G.load_shop_vouchers = nil + else + local vouchers_to_spawn = 0 + for _,_ in pairs(G.GAME.current_round.voucher.spawn) do vouchers_to_spawn = vouchers_to_spawn + 1 end + if vouchers_to_spawn < G.GAME.starting_params.vouchers_in_shop + (G.GAME.modifiers.extra_vouchers or 0) then + SMODS.get_next_vouchers(G.GAME.current_round.voucher) + end + for _, key in ipairs(G.GAME.current_round.voucher or {}) do + if G.P_CENTERS[key] and G.GAME.current_round.voucher.spawn[key] then + SMODS.add_voucher_to_shop(key) + end + end + end + + if G.load_shop_booster then + nosave_shop = true + G.shop_booster:load(G.load_shop_booster) + for k, v in ipairs(G.shop_booster.cards) do + create_shop_card_ui(v) + v:start_materialize() + end + G.load_shop_booster = nil + else + for i=1, G.GAME.starting_params.boosters_in_shop + (G.GAME.modifiers.extra_boosters or 0) do + G.GAME.current_round.used_packs = G.GAME.current_round.used_packs or {} + if not G.GAME.current_round.used_packs[i] then + G.GAME.current_round.used_packs[i] = get_pack('shop_pack').key + end + + if G.GAME.current_round.used_packs[i] ~= 'USED' then + local card = Card(G.shop_booster.T.x + G.shop_booster.T.w/2, + G.shop_booster.T.y, G.CARD_W*1.27, G.CARD_H*1.27, G.P_CARDS.empty, G.P_CENTERS[G.GAME.current_round.used_packs[i]], {bypass_discovery_center = true, bypass_discovery_ui = true}) + create_shop_card_ui(card, 'Booster', G.shop_booster) + card.ability.booster_pos = i + card:start_materialize() + G.shop_booster:emplace(card) + end + end + + for i = 1, #G.GAME.tags do + G.GAME.tags[i]:apply_to_run({type = 'voucher_add'}) + end + for i = 1, #G.GAME.tags do + G.GAME.tags[i]:apply_to_run({type = 'shop_final_pass'}) + end + end + end + + if not nosave_shop then SMODS.calculate_context({starting_shop = true}) end + G.CONTROLLER:snap_to({node = G.shop:get_UIE_by_ID('next_round_button')}) + if not nosave_shop then G.E_MANAGER:add_event(Event({ func = function() save_run(); return true end})) end + return true + end + end + })) + return true + end + })) + return true + end + })) + end, + on_exit = function (self, args) + if args.from_hold then + if G.shop and not G.shop.alignment.offset.py then + G.shop.alignment.offset.py = G.shop.alignment.offset.y + G.shop.alignment.offset.y = G.ROOM.T.y + 29 + G.SHOP_SIGN.alignment.offset.y = -15 + end + return + end + stop_use() + G.CONTROLLER.locks.toggle_shop = true + if G.shop then + if not from_hold then + SMODS.calculate_context({ending_shop = true}) + end + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + G.shop.alignment.offset.y = G.ROOM.T.y + 29 + G.SHOP_SIGN.alignment.offset.y = -15 + return true + end + })) + if from_hold then + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 0.5, + func = function () + G.CONTROLLER.locks.toggle_shop = nil + return true + end + })) + return + end + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 0.5, + func = function() + G.shop:remove() + G.shop = nil + G.SHOP_SIGN:remove() + G.SHOP_SIGN = nil + G.STATE_COMPLETE = false + G.CONTROLLER.locks.toggle_shop = nil + return true + end + })) + end + end, + check_win = true, + } + + SMODS.GameState { + key = SMODS.STATES.ROUND_EVAL, + on_enter = function (self, args) + if args.from_hold then + if G.round_eval then + G.round_eval.alignment.offset.y = G.round_eval.alignment.offset.py + G.round_eval.alignment.offset.py = nil + end + return + end + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + func = function () + stop_use() + G.STATE_COMPLETE = true + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + save_run() + ease_background_colour_blind(G.STATES.ROUND_EVAL) + G.round_eval = UIBox{ + definition = create_UIBox_round_evaluation(), + config = {align="bm", offset = {x=0,y=G.ROOM.T.y + 19},major = G.hand, bond = 'Weak'} + } + G.round_eval.alignment.offset.x = 0 + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + if G.round_eval.alignment.offset.y ~= -7.8 then + G.round_eval.alignment.offset.y = -7.8 + else + if math.abs(G.round_eval.T.y - G.round_eval.VT.y) < 3 then + G.ROOM.jiggle = G.ROOM.jiggle + 3 + play_sound('cardFan2') + delay(0.1) + G.FUNCS.evaluate_round() + return true + end + end + end})) + return true + end + })) + return true + end + })) + end, + on_exit = function (self, args) + if args.from_hold then + if G.round_eval and not G.round_eval.alignment.offset.py then + G.round_eval.alignment.offset.py = G.round_eval.alignment.offset.y + G.round_eval.alignment.offset.y = G.ROOM.T.y + 29 + end + return + end + stop_use() + if G.round_eval then + G.round_eval.alignment.offset.y = G.ROOM.T.y + 15 + G.round_eval.alignment.offset.x = 0 + G.deck:shuffle('cashout'..G.GAME.round_resets.ante) + G.deck:hard_set_T() + delay(0.3) + G.GAME.current_round.discards_left = math.max(0, G.GAME.round_resets.discards + G.GAME.round_bonus.discards) + G.GAME.current_round.hands_left = (math.max(1, G.GAME.round_resets.hands + G.GAME.round_bonus.next_hands)) + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + if G.round_eval then + G.round_eval:remove() + G.round_eval = nil + end + -- G.STATE_COMPLETE = false + return true + end + })) + ease_dollars(G.GAME.current_round.dollars) + G.E_MANAGER:add_event(Event({ + func = function() + G.GAME.previous_round.dollars = G.GAME.dollars + return true + end + })) + play_sound("coin7") + G.VIBRATION = G.VIBRATION + 1 + end + + ease_chips(0) + if G.GAME.round_resets.blind_states.Boss == 'Defeated' then + G.GAME.round_resets.blind_ante = G.GAME.round_resets.ante + G.GAME.round_resets.blind_tags.Small = get_next_tag_key() + G.GAME.round_resets.blind_tags.Big = get_next_tag_key() + end + reset_blinds() + delay(0.6) + end, + check_win = true, + } + + SMODS.GameState { + key = SMODS.STATES.BLIND, + on_enter = function (self, args) + if args.from_hold then + -- Todo: Implement + return + end + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + func = function () + stop_use() + G.GAME.facing_blind = true + + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + ease_round(1) + inc_career_stat('c_rounds', 1) + if _DEMO then + G.SETTINGS.DEMO_ROUNDS = (G.SETTINGS.DEMO_ROUNDS or 0) + 1 + inc_steam_stat('demo_rounds') + G:save_settings() + end + G.GAME.round_resets.blind = G.P_BLINDS[args.key] + G.GAME.round_resets.blind_states[G.GAME.blind_on_deck] = 'Current' -- TODO : Check this / G.GAME.blind_on_deck + delay(0.2) + return true + end})) + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + new_round() + return true + end + })) + return true + end + })) + end, + on_exit = function (self, args) + G.GAME.facing_blind = nil + if args.no_defeat then -- Example: SMODS.enter_state(SMODS.STATES.SHOP, nil, {no_defeat = true}) -> This opens the shop without defeating the blind or holding SMODS.STATES.BLIND + -- Todo: implement this + return + end + if args.from_hold then + -- Todo: implement holding SMODS.STATES.BLIND + return + end + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + blocking = false, + func = function () + if args.new_state == SMODS.STATES.ROUND_EVAL then -- Precise vanilla timing + if G.round_eval and G.round_eval.alignment.offset.y == -7.8 and math.abs(G.round_eval.T.y - G.round_eval.VT.y) < 3 then + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + func = function () + G.E_MANAGER:add_event(Event({ + trigger = 'before', + delay = 1.3*math.min(G.GAME.blind.dollars+2, 7)/2*0.15 + 0.5, + func = function() + G.GAME.blind:defeat() + return true + end + })) + delay(0.2) + return true + end + })) + return true + end + else + G.FUNCS.draw_from_hand_to_discard() + G.FUNCS.draw_from_discard_to_deck() + G.E_MANAGER:add_event(Event({ + trigger = "after", + blockable = false, + delay = 0.7, + func = function () + G.GAME.blind:defeat() + return true + end + })) + delay(0.4) + return true + end + end + })) + end, + ease_background_colour = function (self, blind_override) + local blindname = ((blind_override or (G.GAME.blind and G.GAME.blind.name ~= '' and G.GAME.blind.name)) or 'Small Blind') + blindname = (blindname == '' and 'Small Blind' or blindname) + + local boss_col = G.C.BLACK + for k, v in pairs(G.P_BLINDS) do + if v.name == blindname then + if v.boss and v.boss.showdown or v.blind_types and v.blind_types.Showdown then + ease_background_colour{new_colour = G.C.BLUE, special_colour = G.C.RED, tertiary_colour = darken(G.C.BLACK, 0.4), contrast = 3} + return + end + boss_col = v.boss_colour or G.C.BLACK + end + end + ease_background_colour{new_colour = lighten(mix_colours(boss_col, G.C.BLACK, 0.3), 0.1), special_colour = boss_col, contrast = 2} + end + } + + SMODS.GameState { + key = SMODS.STATES.BLIND_SELECT, + on_enter = function (self, args) + if args.from_hold then + if G.blind_select then + G.blind_select.alignment.offset.y = G.blind_select.alignment.offset.py + G.blind_select.alignment.offset.py = nil + end + return + end + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + func = function() + stop_use() + ease_background_colour_blind(SMODS.STATES.BLIND_SELECT) + G.E_MANAGER:add_event(Event({ func = function() save_run(); return true end})) + G.CONTROLLER.interrupt.focus = true + G.E_MANAGER:add_event(Event({ func = function() + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + play_sound('cancel') + G.blind_select = UIBox{ + definition = create_UIBox_blind_select(), + config = {align="bmi", offset = {x=0,y=G.ROOM.T.y + 29},major = G.hand, bond = 'Weak'} + } + G.blind_select.alignment.offset.y = 0.8-(G.hand.T.y - G.jokers.T.y) + G.blind_select.T.h + G.ROOM.jiggle = G.ROOM.jiggle + 3 + G.blind_select.alignment.offset.x = 0 + G.CONTROLLER.lock_input = false + for i = 1, #G.GAME.tags do + G.GAME.tags[i]:apply_to_run({type = 'immediate'}) + end + for i = 1, #G.GAME.tags do + if G.GAME.tags[i]:apply_to_run({type = 'new_blind_choice'}) then break end + end + return true + end + })) + return true + end})) + return true + end + })) + end, + on_exit = function (self, args) + if args.from_hold then + if G.blind_select and not G.blind_select.alignment.offset.py then + G.blind_select.alignment.offset.py = G.blind_select.alignment.offset.y + G.blind_select.alignment.offset.y = G.ROOM.T.y + 39 + end + return + end + G.blind_prompt_box:get_UIE_by_ID('prompt_dynatext1').config.object.pop_delay = 0 + G.blind_prompt_box:get_UIE_by_ID('prompt_dynatext1').config.object:pop_out(5) + G.blind_prompt_box:get_UIE_by_ID('prompt_dynatext2').config.object.pop_delay = 0 + G.blind_prompt_box:get_UIE_by_ID('prompt_dynatext2').config.object:pop_out(5) + + G.E_MANAGER:add_event(Event({ + trigger = 'before', delay = 0.2, + func = function() + G.blind_prompt_box.alignment.offset.y = -10 + G.blind_select.alignment.offset.y = 40 + G.blind_select.alignment.offset.x = 0 + return true + end})) + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + func = function () + G.blind_select:remove() + G.blind_prompt_box:remove() + G.blind_select = nil + return true + end + })) + + end, + check_win = true, + } \ No newline at end of file From 0816babe5fbd3b2a27623bef91e4fec064b4baf8 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Sat, 4 Apr 2026 22:48:47 +0200 Subject: [PATCH 11/39] Forgot to load the new file *Title, :)) --- src/game_object.lua | 6 + src/game_objects/game_states.lua | 1024 +++++++++++++++--------------- 2 files changed, 516 insertions(+), 514 deletions(-) diff --git a/src/game_object.lua b/src/game_object.lua index 176442c01..a8db4f1a2 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -4125,6 +4125,12 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. text = '^' } + ------------------------------------------------------------------------------------------------- + ----- API CODE SMODS.GameState + ------------------------------------------------------------------------------------------------- + + assert(load(SMODS.NFS.read(SMODS.path..'src/game_objects/game_states.lua'), ('=[SMODS _ "src/game_objects/game_states.lua"]')))() + ------------------------------------------------------------------------------------------------- ----- API IMPORT GameObject.DrawStep ------------------------------------------------------------------------------------------------- diff --git a/src/game_objects/game_states.lua b/src/game_objects/game_states.lua index b4cea85af..6678588ef 100644 --- a/src/game_objects/game_states.lua +++ b/src/game_objects/game_states.lua @@ -1,597 +1,593 @@ - ------------------------------------------------------------------------------------------------- - ----- API CODE SMODS.GameState - ------------------------------------------------------------------------------------------------- +SMODS.STATES = { + BOOSTER_OPENED = "BOOSTER_OPENED", + REDEEM_VOUCHER = "REDEEM_VOUCHER", + SHOP = "SHOP", + ROUND_EVAL = "ROUND_EVAL", + BLIND = "BLIND", + BLIND_SELECT = "BLIND_SELECT" +} +SMODS.default_state = SMODS.STATES.BLIND_SELECT - SMODS.STATES = { - BOOSTER_OPENED = "BOOSTER_OPENED", - REDEEM_VOUCHER = "REDEEM_VOUCHER", - SHOP = "SHOP", - ROUND_EVAL = "ROUND_EVAL", - BLIND = "BLIND", - BLIND_SELECT = "BLIND_SELECT" - } - SMODS.default_state = SMODS.STATES.BLIND_SELECT +SMODS.state_stack = {} - SMODS.state_stack = {} +function SMODS.get_current_state() + return #SMODS.state_stack > 0 and SMODS.state_stack[#SMODS.state_stack] +end - function SMODS.get_current_state() - return #SMODS.state_stack > 0 and SMODS.state_stack[#SMODS.state_stack] - end +function SMODS.push_to_state_stack(state, args) + local data = SMODS.GameStates[state] and SMODS.GameStates[state].store_to_stack() + table.insert(SMODS.state_stack, {state=state, args=args, data=data}) +end - function SMODS.push_to_state_stack(state, args) - local data = SMODS.GameStates[state] and SMODS.GameStates[state].store_to_stack() - table.insert(SMODS.state_stack, {state=state, args=args, data=data}) +function SMODS.pop_from_state_stack(state) + if #SMODS.state_stack < 1 then return end + if SMODS.state_stack[#SMODS.state_stack].state == state then + table.remove(SMODS.state_stack, #SMODS.state_stack) end +end + +function SMODS.clear_state_stack() + SMODS.state_stack = {} +end - function SMODS.pop_from_state_stack(state) - if #SMODS.state_stack < 1 then return end - if SMODS.state_stack[#SMODS.state_stack].state == state then - table.remove(SMODS.state_stack, #SMODS.state_stack) +function SMODS.enter_state(new_state, enter_args, exit_args) + local current_state = SMODS.STATE or G.STATE -- It only handles on_exit() and on_enter() for SMODS.GameState states + if current_state == new_state then return end + enter_args = enter_args or {} + exit_args = exit_args or {} + exit_args.new_state = exit_args.new_state or new_state + if SMODS.GameStates[current_state] then + SMODS.GameStates[current_state]:on_exit(exit_args) + if not exit_args.from_hold then + SMODS.pop_from_state_stack(current_state) end end - - function SMODS.clear_state_stack() - SMODS.state_stack = {} + G.STATE = new_state + if SMODS.GameStates[new_state] then + SMODS.STATE = new_state + SMODS.push_to_state_stack(new_state, enter_args) + SMODS.GameStates[new_state]:on_enter(enter_args) end +end - function SMODS.enter_state(new_state, enter_args, exit_args) - local current_state = SMODS.STATE or G.STATE -- It only handles on_exit() and on_enter() for SMODS.GameState states - if current_state == new_state then return end - enter_args = enter_args or {} - exit_args = exit_args or {} - exit_args.new_state = exit_args.new_state or new_state - if SMODS.GameStates[current_state] then - SMODS.GameStates[current_state]:on_exit(exit_args) - if not exit_args.from_hold then - SMODS.pop_from_state_stack(current_state) - end - end - G.STATE = new_state - if SMODS.GameStates[new_state] then - SMODS.STATE = new_state - SMODS.push_to_state_stack(new_state, enter_args) - SMODS.GameStates[new_state]:on_enter(enter_args) - end +function SMODS.exit_state(exit_args, enter_args, default) + local current_state = SMODS.STATE or G.STATE + local new_state + if #SMODS.state_stack < 2 then + new_state = default.state_override or SMODS.default_state + else + new_state = SMODS.state_stack[#SMODS.state_stack - 1].state end - - function SMODS.exit_state(exit_args, enter_args, default) - local current_state = SMODS.STATE or G.STATE - local new_state - if #SMODS.state_stack < 2 then - new_state = default.state_override or SMODS.default_state - else - new_state = SMODS.state_stack[#SMODS.state_stack - 1].state - end - exit_args = exit_args or {} - exit_args.new_state = exit_args.new_state or new_state - enter_args = enter_args or {} - enter_args.from_hold = true - default = default or {} - default.enter_args = default.enter_args or {} - if SMODS.GameStates[current_state] then - SMODS.GameStates[current_state]:on_exit(exit_args) - SMODS.pop_from_state_stack(current_state) - end - if #SMODS.state_stack < 1 then - G.STATE = nil - SMODS.STATE = nil - SMODS.enter_state(new_state, default.enter_args) - return - end - G.STATE = new_state - if SMODS.GameStates[G.STATE] then - SMODS.STATE = G.STATE - SMODS.GameStates[G.STATE]:on_enter(enter_args) - end + exit_args = exit_args or {} + exit_args.new_state = exit_args.new_state or new_state + enter_args = enter_args or {} + enter_args.from_hold = true + default = default or {} + default.enter_args = default.enter_args or {} + if SMODS.GameStates[current_state] then + SMODS.GameStates[current_state]:on_exit(exit_args) + SMODS.pop_from_state_stack(current_state) end - - local delete_run_ref = Game.delete_run - function Game:delete_run() - local ret = delete_run_ref(self) + if #SMODS.state_stack < 1 then + G.STATE = nil SMODS.STATE = nil - SMODS.clear_state_stack() - return ret + SMODS.enter_state(new_state, default.enter_args) + return + end + G.STATE = new_state + if SMODS.GameStates[G.STATE] then + SMODS.STATE = G.STATE + SMODS.GameStates[G.STATE]:on_enter(enter_args) end +end - SMODS.GameStates = {} - SMODS.GameState = SMODS.GameObject:extend{ - set = 'GameState', - obj_table = SMODS.GameStates, - obj_buffer = {}, - required_parameters = { - 'key', - }, - inject = function (self, i) - - end, - on_enter = function (self, args) end, - on_exit = function (self, args) end, - update = function (self, dt) end, - store_to_stack = function () end, -- Called when SMODS.push_to_state_stack() is called for this GameState, allows returning thus storing per-instance state data, to restore when e.g. re-entering from being held. - ease_background_colour = nil, -- function - exit_after_use_card = false, -- Used for consumable states like SMODS.STATES.REDEEM_VOUCHER - exit_after_end_consumable = false, -- Used for booster-like states like SMODS.STATES.BOOSTER_OPENED - } +local delete_run_ref = Game.delete_run +function Game:delete_run() + local ret = delete_run_ref(self) + SMODS.STATE = nil + SMODS.clear_state_stack() + return ret +end - SMODS.GameState { - key = SMODS.STATES.BOOSTER_OPENED, - update = function (self, dt) - SMODS.OPENED_BOOSTER.config.center:update_pack(dt) - end, - exit_after_end_consumable = true, - } +SMODS.GameStates = {} +SMODS.GameState = SMODS.GameObject:extend{ + set = 'GameState', + obj_table = SMODS.GameStates, + obj_buffer = {}, + required_parameters = { + 'key', + }, + inject = function (self, i) + + end, + on_enter = function (self, args) end, + on_exit = function (self, args) end, + update = function (self, dt) end, + store_to_stack = function () end, -- Called when SMODS.push_to_state_stack() is called for this GameState, allows returning thus storing per-instance state data, to restore when e.g. re-entering from being held. + ease_background_colour = nil, -- function + exit_after_use_card = false, -- Used for consumable states like SMODS.STATES.REDEEM_VOUCHER + exit_after_end_consumable = false, -- Used for booster-like states like SMODS.STATES.BOOSTER_OPENED +} - SMODS.GameState { - key = SMODS.STATES.REDEEM_VOUCHER, - exit_after_use_card = true, - } +SMODS.GameState { + key = SMODS.STATES.BOOSTER_OPENED, + update = function (self, dt) + SMODS.OPENED_BOOSTER.config.center:update_pack(dt) + end, + exit_after_end_consumable = true, +} - SMODS.GameState { - key = SMODS.STATES.SHOP, - on_enter = function (self, args) - if args.from_hold then - -- Extracted from G.FUNCS.use_card() - -- Todo : Make better - if G.shop then - G.shop.alignment.offset.y = G.shop.alignment.offset.py - G.shop.alignment.offset.py = nil - G.SHOP_SIGN.alignment.offset.y = 0 - end - return +SMODS.GameState { + key = SMODS.STATES.REDEEM_VOUCHER, + exit_after_use_card = true, +} + +SMODS.GameState { + key = SMODS.STATES.SHOP, + on_enter = function (self, args) + if args.from_hold then + -- Extracted from G.FUNCS.use_card() + -- Todo : Make better + if G.shop then + G.shop.alignment.offset.y = G.shop.alignment.offset.py + G.shop.alignment.offset.py = nil + G.SHOP_SIGN.alignment.offset.y = 0 end - G.E_MANAGER:add_event(Event({ - trigger = "immediate", - func = function () - stop_use() - G.STATE_COMPLETE = true - ease_background_colour_blind(G.STATES.SHOP) - local shop_exists = not not G.shop - G.shop = G.shop or UIBox{ - definition = G.UIDEF.shop(), - config = {align='tmi', offset = {x=0,y=G.ROOM.T.y+11},major = G.hand, bond = 'Weak'} - } - -- Moved here from G.FUNCS.cash_out() - G.GAME.current_round.jokers_purchased = 0 - G.GAME.shop_free = nil - G.GAME.shop_d6ed = nil - ------- - G.E_MANAGER:add_event(Event({ - func = function() - G.shop.alignment.offset.y = -5.3 - G.shop.alignment.offset.x = 0 - G.E_MANAGER:add_event(Event({ - trigger = 'after', - delay = 0.2, - blockable = false, - func = function() - if math.abs(G.shop.T.y - G.shop.VT.y) < 3 then - G.ROOM.jiggle = G.ROOM.jiggle + 3 - play_sound('cardFan2') - for i = 1, #G.GAME.tags do - G.GAME.tags[i]:apply_to_run({type = 'shop_start'}) - end - local nosave_shop = nil - if not shop_exists then - if G.load_shop_jokers then - nosave_shop = true - G.shop_jokers:load(G.load_shop_jokers) - for k, v in ipairs(G.shop_jokers.cards) do - create_shop_card_ui(v) - if v.ability.consumeable then v:start_materialize() end - for _kk, vvv in ipairs(G.GAME.tags) do - if vvv:apply_to_run({type = 'store_joker_modify', card = v}) then break end - end - end - G.load_shop_jokers = nil - else - for i = 1, G.GAME.shop.joker_max - #G.shop_jokers.cards do - G.shop_jokers:emplace(create_card_for_shop(G.shop_jokers)) + return + end + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + func = function () + stop_use() + G.STATE_COMPLETE = true + ease_background_colour_blind(G.STATES.SHOP) + local shop_exists = not not G.shop + G.shop = G.shop or UIBox{ + definition = G.UIDEF.shop(), + config = {align='tmi', offset = {x=0,y=G.ROOM.T.y+11},major = G.hand, bond = 'Weak'} + } + -- Moved here from G.FUNCS.cash_out() + G.GAME.current_round.jokers_purchased = 0 + G.GAME.shop_free = nil + G.GAME.shop_d6ed = nil + ------- + G.E_MANAGER:add_event(Event({ + func = function() + G.shop.alignment.offset.y = -5.3 + G.shop.alignment.offset.x = 0 + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 0.2, + blockable = false, + func = function() + if math.abs(G.shop.T.y - G.shop.VT.y) < 3 then + G.ROOM.jiggle = G.ROOM.jiggle + 3 + play_sound('cardFan2') + for i = 1, #G.GAME.tags do + G.GAME.tags[i]:apply_to_run({type = 'shop_start'}) + end + local nosave_shop = nil + if not shop_exists then + if G.load_shop_jokers then + nosave_shop = true + G.shop_jokers:load(G.load_shop_jokers) + for k, v in ipairs(G.shop_jokers.cards) do + create_shop_card_ui(v) + if v.ability.consumeable then v:start_materialize() end + for _kk, vvv in ipairs(G.GAME.tags) do + if vvv:apply_to_run({type = 'store_joker_modify', card = v}) then break end end end + G.load_shop_jokers = nil + else + for i = 1, G.GAME.shop.joker_max - #G.shop_jokers.cards do + G.shop_jokers:emplace(create_card_for_shop(G.shop_jokers)) + end + end - if G.load_shop_vouchers then - nosave_shop = true - G.shop_vouchers:load(G.load_shop_vouchers) - for k, v in ipairs(G.shop_vouchers.cards) do - create_shop_card_ui(v) - v:start_materialize() - end - G.load_shop_vouchers = nil - else - local vouchers_to_spawn = 0 - for _,_ in pairs(G.GAME.current_round.voucher.spawn) do vouchers_to_spawn = vouchers_to_spawn + 1 end - if vouchers_to_spawn < G.GAME.starting_params.vouchers_in_shop + (G.GAME.modifiers.extra_vouchers or 0) then - SMODS.get_next_vouchers(G.GAME.current_round.voucher) - end - for _, key in ipairs(G.GAME.current_round.voucher or {}) do - if G.P_CENTERS[key] and G.GAME.current_round.voucher.spawn[key] then - SMODS.add_voucher_to_shop(key) - end + if G.load_shop_vouchers then + nosave_shop = true + G.shop_vouchers:load(G.load_shop_vouchers) + for k, v in ipairs(G.shop_vouchers.cards) do + create_shop_card_ui(v) + v:start_materialize() + end + G.load_shop_vouchers = nil + else + local vouchers_to_spawn = 0 + for _,_ in pairs(G.GAME.current_round.voucher.spawn) do vouchers_to_spawn = vouchers_to_spawn + 1 end + if vouchers_to_spawn < G.GAME.starting_params.vouchers_in_shop + (G.GAME.modifiers.extra_vouchers or 0) then + SMODS.get_next_vouchers(G.GAME.current_round.voucher) + end + for _, key in ipairs(G.GAME.current_round.voucher or {}) do + if G.P_CENTERS[key] and G.GAME.current_round.voucher.spawn[key] then + SMODS.add_voucher_to_shop(key) end end + end - if G.load_shop_booster then - nosave_shop = true - G.shop_booster:load(G.load_shop_booster) - for k, v in ipairs(G.shop_booster.cards) do - create_shop_card_ui(v) - v:start_materialize() + if G.load_shop_booster then + nosave_shop = true + G.shop_booster:load(G.load_shop_booster) + for k, v in ipairs(G.shop_booster.cards) do + create_shop_card_ui(v) + v:start_materialize() + end + G.load_shop_booster = nil + else + for i=1, G.GAME.starting_params.boosters_in_shop + (G.GAME.modifiers.extra_boosters or 0) do + G.GAME.current_round.used_packs = G.GAME.current_round.used_packs or {} + if not G.GAME.current_round.used_packs[i] then + G.GAME.current_round.used_packs[i] = get_pack('shop_pack').key end - G.load_shop_booster = nil - else - for i=1, G.GAME.starting_params.boosters_in_shop + (G.GAME.modifiers.extra_boosters or 0) do - G.GAME.current_round.used_packs = G.GAME.current_round.used_packs or {} - if not G.GAME.current_round.used_packs[i] then - G.GAME.current_round.used_packs[i] = get_pack('shop_pack').key - end - if G.GAME.current_round.used_packs[i] ~= 'USED' then - local card = Card(G.shop_booster.T.x + G.shop_booster.T.w/2, - G.shop_booster.T.y, G.CARD_W*1.27, G.CARD_H*1.27, G.P_CARDS.empty, G.P_CENTERS[G.GAME.current_round.used_packs[i]], {bypass_discovery_center = true, bypass_discovery_ui = true}) - create_shop_card_ui(card, 'Booster', G.shop_booster) - card.ability.booster_pos = i - card:start_materialize() - G.shop_booster:emplace(card) - end + if G.GAME.current_round.used_packs[i] ~= 'USED' then + local card = Card(G.shop_booster.T.x + G.shop_booster.T.w/2, + G.shop_booster.T.y, G.CARD_W*1.27, G.CARD_H*1.27, G.P_CARDS.empty, G.P_CENTERS[G.GAME.current_round.used_packs[i]], {bypass_discovery_center = true, bypass_discovery_ui = true}) + create_shop_card_ui(card, 'Booster', G.shop_booster) + card.ability.booster_pos = i + card:start_materialize() + G.shop_booster:emplace(card) end + end - for i = 1, #G.GAME.tags do - G.GAME.tags[i]:apply_to_run({type = 'voucher_add'}) - end - for i = 1, #G.GAME.tags do - G.GAME.tags[i]:apply_to_run({type = 'shop_final_pass'}) - end + for i = 1, #G.GAME.tags do + G.GAME.tags[i]:apply_to_run({type = 'voucher_add'}) + end + for i = 1, #G.GAME.tags do + G.GAME.tags[i]:apply_to_run({type = 'shop_final_pass'}) end end - - if not nosave_shop then SMODS.calculate_context({starting_shop = true}) end - G.CONTROLLER:snap_to({node = G.shop:get_UIE_by_ID('next_round_button')}) - if not nosave_shop then G.E_MANAGER:add_event(Event({ func = function() save_run(); return true end})) end - return true end + + if not nosave_shop then SMODS.calculate_context({starting_shop = true}) end + G.CONTROLLER:snap_to({node = G.shop:get_UIE_by_ID('next_round_button')}) + if not nosave_shop then G.E_MANAGER:add_event(Event({ func = function() save_run(); return true end})) end + return true end - })) - return true - end - })) - return true - end - })) - end, - on_exit = function (self, args) - if args.from_hold then - if G.shop and not G.shop.alignment.offset.py then - G.shop.alignment.offset.py = G.shop.alignment.offset.y - G.shop.alignment.offset.y = G.ROOM.T.y + 29 - G.SHOP_SIGN.alignment.offset.y = -15 - end - return - end - stop_use() - G.CONTROLLER.locks.toggle_shop = true - if G.shop then - if not from_hold then - SMODS.calculate_context({ending_shop = true}) - end - G.E_MANAGER:add_event(Event({ - trigger = 'immediate', - func = function() - G.shop.alignment.offset.y = G.ROOM.T.y + 29 - G.SHOP_SIGN.alignment.offset.y = -15 + end + })) return true end })) - if from_hold then - G.E_MANAGER:add_event(Event({ - trigger = 'after', - delay = 0.5, - func = function () - G.CONTROLLER.locks.toggle_shop = nil - return true - end - })) - return + return true + end + })) + end, + on_exit = function (self, args) + if args.from_hold then + if G.shop and not G.shop.alignment.offset.py then + G.shop.alignment.offset.py = G.shop.alignment.offset.y + G.shop.alignment.offset.y = G.ROOM.T.y + 29 + G.SHOP_SIGN.alignment.offset.y = -15 + end + return + end + stop_use() + G.CONTROLLER.locks.toggle_shop = true + if G.shop then + if not from_hold then + SMODS.calculate_context({ending_shop = true}) + end + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + G.shop.alignment.offset.y = G.ROOM.T.y + 29 + G.SHOP_SIGN.alignment.offset.y = -15 + return true end + })) + if from_hold then G.E_MANAGER:add_event(Event({ trigger = 'after', delay = 0.5, - func = function() - G.shop:remove() - G.shop = nil - G.SHOP_SIGN:remove() - G.SHOP_SIGN = nil - G.STATE_COMPLETE = false + func = function () G.CONTROLLER.locks.toggle_shop = nil return true end })) - end - end, - check_win = true, - } - - SMODS.GameState { - key = SMODS.STATES.ROUND_EVAL, - on_enter = function (self, args) - if args.from_hold then - if G.round_eval then - G.round_eval.alignment.offset.y = G.round_eval.alignment.offset.py - G.round_eval.alignment.offset.py = nil - end return end G.E_MANAGER:add_event(Event({ - trigger = "immediate", - func = function () - stop_use() - G.STATE_COMPLETE = true - G.E_MANAGER:add_event(Event({ - trigger = 'immediate', - func = function() - save_run() - ease_background_colour_blind(G.STATES.ROUND_EVAL) - G.round_eval = UIBox{ - definition = create_UIBox_round_evaluation(), - config = {align="bm", offset = {x=0,y=G.ROOM.T.y + 19},major = G.hand, bond = 'Weak'} - } - G.round_eval.alignment.offset.x = 0 - G.E_MANAGER:add_event(Event({ - trigger = 'immediate', - func = function() - if G.round_eval.alignment.offset.y ~= -7.8 then - G.round_eval.alignment.offset.y = -7.8 - else - if math.abs(G.round_eval.T.y - G.round_eval.VT.y) < 3 then - G.ROOM.jiggle = G.ROOM.jiggle + 3 - play_sound('cardFan2') - delay(0.1) - G.FUNCS.evaluate_round() - return true - end - end - end})) - return true - end - })) + trigger = 'after', + delay = 0.5, + func = function() + G.shop:remove() + G.shop = nil + G.SHOP_SIGN:remove() + G.SHOP_SIGN = nil + G.STATE_COMPLETE = false + G.CONTROLLER.locks.toggle_shop = nil return true end })) - end, - on_exit = function (self, args) - if args.from_hold then - if G.round_eval and not G.round_eval.alignment.offset.py then - G.round_eval.alignment.offset.py = G.round_eval.alignment.offset.y - G.round_eval.alignment.offset.y = G.ROOM.T.y + 29 - end - return - end - stop_use() + end + end, + check_win = true, +} + +SMODS.GameState { + key = SMODS.STATES.ROUND_EVAL, + on_enter = function (self, args) + if args.from_hold then if G.round_eval then - G.round_eval.alignment.offset.y = G.ROOM.T.y + 15 - G.round_eval.alignment.offset.x = 0 - G.deck:shuffle('cashout'..G.GAME.round_resets.ante) - G.deck:hard_set_T() - delay(0.3) - G.GAME.current_round.discards_left = math.max(0, G.GAME.round_resets.discards + G.GAME.round_bonus.discards) - G.GAME.current_round.hands_left = (math.max(1, G.GAME.round_resets.hands + G.GAME.round_bonus.next_hands)) + G.round_eval.alignment.offset.y = G.round_eval.alignment.offset.py + G.round_eval.alignment.offset.py = nil + end + return + end + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + func = function () + stop_use() + G.STATE_COMPLETE = true G.E_MANAGER:add_event(Event({ trigger = 'immediate', func = function() - if G.round_eval then - G.round_eval:remove() - G.round_eval = nil - end - -- G.STATE_COMPLETE = false - return true - end - })) - ease_dollars(G.GAME.current_round.dollars) - G.E_MANAGER:add_event(Event({ - func = function() - G.GAME.previous_round.dollars = G.GAME.dollars + save_run() + ease_background_colour_blind(G.STATES.ROUND_EVAL) + G.round_eval = UIBox{ + definition = create_UIBox_round_evaluation(), + config = {align="bm", offset = {x=0,y=G.ROOM.T.y + 19},major = G.hand, bond = 'Weak'} + } + G.round_eval.alignment.offset.x = 0 + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + if G.round_eval.alignment.offset.y ~= -7.8 then + G.round_eval.alignment.offset.y = -7.8 + else + if math.abs(G.round_eval.T.y - G.round_eval.VT.y) < 3 then + G.ROOM.jiggle = G.ROOM.jiggle + 3 + play_sound('cardFan2') + delay(0.1) + G.FUNCS.evaluate_round() + return true + end + end + end})) return true end })) - play_sound("coin7") - G.VIBRATION = G.VIBRATION + 1 + return true end - - ease_chips(0) - if G.GAME.round_resets.blind_states.Boss == 'Defeated' then - G.GAME.round_resets.blind_ante = G.GAME.round_resets.ante - G.GAME.round_resets.blind_tags.Small = get_next_tag_key() - G.GAME.round_resets.blind_tags.Big = get_next_tag_key() - end - reset_blinds() - delay(0.6) - end, - check_win = true, - } - - SMODS.GameState { - key = SMODS.STATES.BLIND, - on_enter = function (self, args) - if args.from_hold then - -- Todo: Implement - return + })) + end, + on_exit = function (self, args) + if args.from_hold then + if G.round_eval and not G.round_eval.alignment.offset.py then + G.round_eval.alignment.offset.py = G.round_eval.alignment.offset.y + G.round_eval.alignment.offset.y = G.ROOM.T.y + 29 end + return + end + stop_use() + if G.round_eval then + G.round_eval.alignment.offset.y = G.ROOM.T.y + 15 + G.round_eval.alignment.offset.x = 0 + G.deck:shuffle('cashout'..G.GAME.round_resets.ante) + G.deck:hard_set_T() + delay(0.3) + G.GAME.current_round.discards_left = math.max(0, G.GAME.round_resets.discards + G.GAME.round_bonus.discards) + G.GAME.current_round.hands_left = (math.max(1, G.GAME.round_resets.hands + G.GAME.round_bonus.next_hands)) G.E_MANAGER:add_event(Event({ - trigger = "immediate", - func = function () - stop_use() - G.GAME.facing_blind = true - - G.E_MANAGER:add_event(Event({ - trigger = 'immediate', - func = function() - ease_round(1) - inc_career_stat('c_rounds', 1) - if _DEMO then - G.SETTINGS.DEMO_ROUNDS = (G.SETTINGS.DEMO_ROUNDS or 0) + 1 - inc_steam_stat('demo_rounds') - G:save_settings() - end - G.GAME.round_resets.blind = G.P_BLINDS[args.key] - G.GAME.round_resets.blind_states[G.GAME.blind_on_deck] = 'Current' -- TODO : Check this / G.GAME.blind_on_deck - delay(0.2) - return true - end})) - G.E_MANAGER:add_event(Event({ - trigger = 'immediate', - func = function() - new_round() - return true - end - })) + trigger = 'immediate', + func = function() + if G.round_eval then + G.round_eval:remove() + G.round_eval = nil + end + -- G.STATE_COMPLETE = false return true end })) - end, - on_exit = function (self, args) - G.GAME.facing_blind = nil - if args.no_defeat then -- Example: SMODS.enter_state(SMODS.STATES.SHOP, nil, {no_defeat = true}) -> This opens the shop without defeating the blind or holding SMODS.STATES.BLIND - -- Todo: implement this - return - end - if args.from_hold then - -- Todo: implement holding SMODS.STATES.BLIND - return - end + ease_dollars(G.GAME.current_round.dollars) G.E_MANAGER:add_event(Event({ - trigger = "immediate", - blocking = false, - func = function () - if args.new_state == SMODS.STATES.ROUND_EVAL then -- Precise vanilla timing - if G.round_eval and G.round_eval.alignment.offset.y == -7.8 and math.abs(G.round_eval.T.y - G.round_eval.VT.y) < 3 then - G.E_MANAGER:add_event(Event({ - trigger = "immediate", - func = function () - G.E_MANAGER:add_event(Event({ - trigger = 'before', - delay = 1.3*math.min(G.GAME.blind.dollars+2, 7)/2*0.15 + 0.5, - func = function() - G.GAME.blind:defeat() - return true - end - })) - delay(0.2) - return true - end - })) - return true + func = function() + G.GAME.previous_round.dollars = G.GAME.dollars + return true + end + })) + play_sound("coin7") + G.VIBRATION = G.VIBRATION + 1 + end + + ease_chips(0) + if G.GAME.round_resets.blind_states.Boss == 'Defeated' then + G.GAME.round_resets.blind_ante = G.GAME.round_resets.ante + G.GAME.round_resets.blind_tags.Small = get_next_tag_key() + G.GAME.round_resets.blind_tags.Big = get_next_tag_key() + end + reset_blinds() + delay(0.6) + end, + check_win = true, +} + +SMODS.GameState { + key = SMODS.STATES.BLIND, + on_enter = function (self, args) + if args.from_hold then + -- Todo: Implement + return + end + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + func = function () + stop_use() + G.GAME.facing_blind = true + + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + ease_round(1) + inc_career_stat('c_rounds', 1) + if _DEMO then + G.SETTINGS.DEMO_ROUNDS = (G.SETTINGS.DEMO_ROUNDS or 0) + 1 + inc_steam_stat('demo_rounds') + G:save_settings() end - else - G.FUNCS.draw_from_hand_to_discard() - G.FUNCS.draw_from_discard_to_deck() + G.GAME.round_resets.blind = G.P_BLINDS[args.key] + G.GAME.round_resets.blind_states[G.GAME.blind_on_deck] = 'Current' -- TODO : Check this / G.GAME.blind_on_deck + delay(0.2) + return true + end})) + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + new_round() + return true + end + })) + return true + end + })) + end, + on_exit = function (self, args) + G.GAME.facing_blind = nil + if args.no_defeat then -- Example: SMODS.enter_state(SMODS.STATES.SHOP, nil, {no_defeat = true}) -> This opens the shop without defeating the blind or holding SMODS.STATES.BLIND + -- Todo: implement this + return + end + if args.from_hold then + -- Todo: implement holding SMODS.STATES.BLIND + return + end + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + blocking = false, + func = function () + if args.new_state == SMODS.STATES.ROUND_EVAL then -- Precise vanilla timing + if G.round_eval and G.round_eval.alignment.offset.y == -7.8 and math.abs(G.round_eval.T.y - G.round_eval.VT.y) < 3 then G.E_MANAGER:add_event(Event({ - trigger = "after", - blockable = false, - delay = 0.7, + trigger = "immediate", func = function () - G.GAME.blind:defeat() + G.E_MANAGER:add_event(Event({ + trigger = 'before', + delay = 1.3*math.min(G.GAME.blind.dollars+2, 7)/2*0.15 + 0.5, + func = function() + G.GAME.blind:defeat() + return true + end + })) + delay(0.2) return true end })) - delay(0.4) return true end + else + G.FUNCS.draw_from_hand_to_discard() + G.FUNCS.draw_from_discard_to_deck() + G.E_MANAGER:add_event(Event({ + trigger = "after", + blockable = false, + delay = 0.7, + func = function () + G.GAME.blind:defeat() + return true + end + })) + delay(0.4) + return true end - })) - end, - ease_background_colour = function (self, blind_override) - local blindname = ((blind_override or (G.GAME.blind and G.GAME.blind.name ~= '' and G.GAME.blind.name)) or 'Small Blind') - blindname = (blindname == '' and 'Small Blind' or blindname) - - local boss_col = G.C.BLACK - for k, v in pairs(G.P_BLINDS) do - if v.name == blindname then - if v.boss and v.boss.showdown or v.blind_types and v.blind_types.Showdown then - ease_background_colour{new_colour = G.C.BLUE, special_colour = G.C.RED, tertiary_colour = darken(G.C.BLACK, 0.4), contrast = 3} - return - end - boss_col = v.boss_colour or G.C.BLACK + end + })) + end, + ease_background_colour = function (self, blind_override) + local blindname = ((blind_override or (G.GAME.blind and G.GAME.blind.name ~= '' and G.GAME.blind.name)) or 'Small Blind') + blindname = (blindname == '' and 'Small Blind' or blindname) + + local boss_col = G.C.BLACK + for k, v in pairs(G.P_BLINDS) do + if v.name == blindname then + if v.boss and v.boss.showdown or v.blind_types and v.blind_types.Showdown then + ease_background_colour{new_colour = G.C.BLUE, special_colour = G.C.RED, tertiary_colour = darken(G.C.BLACK, 0.4), contrast = 3} + return end + boss_col = v.boss_colour or G.C.BLACK end - ease_background_colour{new_colour = lighten(mix_colours(boss_col, G.C.BLACK, 0.3), 0.1), special_colour = boss_col, contrast = 2} end - } + ease_background_colour{new_colour = lighten(mix_colours(boss_col, G.C.BLACK, 0.3), 0.1), special_colour = boss_col, contrast = 2} + end +} - SMODS.GameState { - key = SMODS.STATES.BLIND_SELECT, - on_enter = function (self, args) - if args.from_hold then - if G.blind_select then - G.blind_select.alignment.offset.y = G.blind_select.alignment.offset.py - G.blind_select.alignment.offset.py = nil - end - return +SMODS.GameState { + key = SMODS.STATES.BLIND_SELECT, + on_enter = function (self, args) + if args.from_hold then + if G.blind_select then + G.blind_select.alignment.offset.y = G.blind_select.alignment.offset.py + G.blind_select.alignment.offset.py = nil end - G.E_MANAGER:add_event(Event({ - trigger = "immediate", - func = function() - stop_use() - ease_background_colour_blind(SMODS.STATES.BLIND_SELECT) - G.E_MANAGER:add_event(Event({ func = function() save_run(); return true end})) - G.CONTROLLER.interrupt.focus = true - G.E_MANAGER:add_event(Event({ func = function() - G.E_MANAGER:add_event(Event({ - trigger = 'immediate', - func = function() - play_sound('cancel') - G.blind_select = UIBox{ - definition = create_UIBox_blind_select(), - config = {align="bmi", offset = {x=0,y=G.ROOM.T.y + 29},major = G.hand, bond = 'Weak'} - } - G.blind_select.alignment.offset.y = 0.8-(G.hand.T.y - G.jokers.T.y) + G.blind_select.T.h - G.ROOM.jiggle = G.ROOM.jiggle + 3 - G.blind_select.alignment.offset.x = 0 - G.CONTROLLER.lock_input = false - for i = 1, #G.GAME.tags do - G.GAME.tags[i]:apply_to_run({type = 'immediate'}) - end - for i = 1, #G.GAME.tags do - if G.GAME.tags[i]:apply_to_run({type = 'new_blind_choice'}) then break end - end - return true + return + end + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + func = function() + stop_use() + ease_background_colour_blind(SMODS.STATES.BLIND_SELECT) + G.E_MANAGER:add_event(Event({ func = function() save_run(); return true end})) + G.CONTROLLER.interrupt.focus = true + G.E_MANAGER:add_event(Event({ func = function() + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + play_sound('cancel') + G.blind_select = UIBox{ + definition = create_UIBox_blind_select(), + config = {align="bmi", offset = {x=0,y=G.ROOM.T.y + 29},major = G.hand, bond = 'Weak'} + } + G.blind_select.alignment.offset.y = 0.8-(G.hand.T.y - G.jokers.T.y) + G.blind_select.T.h + G.ROOM.jiggle = G.ROOM.jiggle + 3 + G.blind_select.alignment.offset.x = 0 + G.CONTROLLER.lock_input = false + for i = 1, #G.GAME.tags do + G.GAME.tags[i]:apply_to_run({type = 'immediate'}) end - })) - return true - end})) + for i = 1, #G.GAME.tags do + if G.GAME.tags[i]:apply_to_run({type = 'new_blind_choice'}) then break end + end + return true + end + })) return true - end - })) - end, - on_exit = function (self, args) - if args.from_hold then - if G.blind_select and not G.blind_select.alignment.offset.py then - G.blind_select.alignment.offset.py = G.blind_select.alignment.offset.y - G.blind_select.alignment.offset.y = G.ROOM.T.y + 39 - end - return + end})) + return true + end + })) + end, + on_exit = function (self, args) + if args.from_hold then + if G.blind_select and not G.blind_select.alignment.offset.py then + G.blind_select.alignment.offset.py = G.blind_select.alignment.offset.y + G.blind_select.alignment.offset.y = G.ROOM.T.y + 39 end - G.blind_prompt_box:get_UIE_by_ID('prompt_dynatext1').config.object.pop_delay = 0 - G.blind_prompt_box:get_UIE_by_ID('prompt_dynatext1').config.object:pop_out(5) - G.blind_prompt_box:get_UIE_by_ID('prompt_dynatext2').config.object.pop_delay = 0 - G.blind_prompt_box:get_UIE_by_ID('prompt_dynatext2').config.object:pop_out(5) + return + end + G.blind_prompt_box:get_UIE_by_ID('prompt_dynatext1').config.object.pop_delay = 0 + G.blind_prompt_box:get_UIE_by_ID('prompt_dynatext1').config.object:pop_out(5) + G.blind_prompt_box:get_UIE_by_ID('prompt_dynatext2').config.object.pop_delay = 0 + G.blind_prompt_box:get_UIE_by_ID('prompt_dynatext2').config.object:pop_out(5) - G.E_MANAGER:add_event(Event({ - trigger = 'before', delay = 0.2, - func = function() - G.blind_prompt_box.alignment.offset.y = -10 - G.blind_select.alignment.offset.y = 40 - G.blind_select.alignment.offset.x = 0 - return true - end})) - G.E_MANAGER:add_event(Event({ - trigger = "immediate", - func = function () - G.blind_select:remove() - G.blind_prompt_box:remove() - G.blind_select = nil - return true - end - })) + G.E_MANAGER:add_event(Event({ + trigger = 'before', delay = 0.2, + func = function() + G.blind_prompt_box.alignment.offset.y = -10 + G.blind_select.alignment.offset.y = 40 + G.blind_select.alignment.offset.x = 0 + return true + end})) + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + func = function () + G.blind_select:remove() + G.blind_prompt_box:remove() + G.blind_select = nil + return true + end + })) - end, - check_win = true, - } \ No newline at end of file + end, + check_win = true, +} \ No newline at end of file From 4f07bffd574205a60c8d79f583603d0baa7d3371 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Sat, 4 Apr 2026 22:52:31 +0200 Subject: [PATCH 12/39] Minor refactoring *Title --- src/game_object.lua | 2 +- src/game_objects/game_states.lua | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/game_object.lua b/src/game_object.lua index a8db4f1a2..150721524 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -4126,7 +4126,7 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. } ------------------------------------------------------------------------------------------------- - ----- API CODE SMODS.GameState + ----- API IMPORT GameObject.GameState ------------------------------------------------------------------------------------------------- assert(load(SMODS.NFS.read(SMODS.path..'src/game_objects/game_states.lua'), ('=[SMODS _ "src/game_objects/game_states.lua"]')))() diff --git a/src/game_objects/game_states.lua b/src/game_objects/game_states.lua index 6678588ef..ffcf64cc5 100644 --- a/src/game_objects/game_states.lua +++ b/src/game_objects/game_states.lua @@ -15,8 +15,7 @@ function SMODS.get_current_state() end function SMODS.push_to_state_stack(state, args) - local data = SMODS.GameStates[state] and SMODS.GameStates[state].store_to_stack() - table.insert(SMODS.state_stack, {state=state, args=args, data=data}) + table.insert(SMODS.state_stack, {state=state, args=args}) end function SMODS.pop_from_state_stack(state) @@ -103,7 +102,6 @@ SMODS.GameState = SMODS.GameObject:extend{ on_enter = function (self, args) end, on_exit = function (self, args) end, update = function (self, dt) end, - store_to_stack = function () end, -- Called when SMODS.push_to_state_stack() is called for this GameState, allows returning thus storing per-instance state data, to restore when e.g. re-entering from being held. ease_background_colour = nil, -- function exit_after_use_card = false, -- Used for consumable states like SMODS.STATES.REDEEM_VOUCHER exit_after_end_consumable = false, -- Used for booster-like states like SMODS.STATES.BOOSTER_OPENED @@ -127,7 +125,7 @@ SMODS.GameState { on_enter = function (self, args) if args.from_hold then -- Extracted from G.FUNCS.use_card() - -- Todo : Make better + -- Todo : Store data to and restore data from SMODS.state_stack if G.shop then G.shop.alignment.offset.y = G.shop.alignment.offset.py G.shop.alignment.offset.py = nil From 852b9324f3e147f6f8c0a738f645cb02d0f0da5a Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Sat, 4 Apr 2026 23:27:39 +0200 Subject: [PATCH 13/39] Readded `G.FUNCS.use_card()` patches +/*Title --- lovely/booster.toml | 150 +++++++++++++++++++++++++++++++ lovely/center.toml | 52 +++++++++++ src/game_objects/game_states.lua | 2 +- 3 files changed, 203 insertions(+), 1 deletion(-) diff --git a/lovely/booster.toml b/lovely/booster.toml index aa4099f89..c3ffb3b3f 100644 --- a/lovely/booster.toml +++ b/lovely/booster.toml @@ -108,6 +108,16 @@ payload = ''' (self.config.type == 'hand' and state == SMODS.STATES.BOOSTER_OPENED) or''' match_indent = true +# G.FUNCS.use_card +[[patches]] +[patches.pattern] +target = "functions/button_callbacks.lua" +pattern = "prev_state == G.STATES.SPECTRAL_PACK or prev_state == G.STATES.STANDARD_PACK or" +position = "after" +payload = ''' +prev_state == SMODS.STATES.BOOSTER_OPENED or''' +match_indent = true + # CardArea:align_cards() [[patches]] [patches.pattern] @@ -135,6 +145,87 @@ position = "at" payload = "if G.STATE == G.STATES.SELECTING_HAND or G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED then" match_indent = true +# G.FUNCS.use_card() +[[patches]] +[patches.pattern] +target = "functions/button_callbacks.lua" +pattern = "if G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.SPECTRAL_PACK then" +position = "at" +payload = """ +if nc then + if area then area:remove_from_highlighted(card) end + play_sound('cardSlide2', nil, 0.3) + dont_dissolve = true +end +if (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED) then""" +match_indent = true + +# G.FUNC.use_card() +[[patches]] +[patches.pattern] +target = "functions/button_callbacks.lua" +pattern = 'if area == G.consumeables then' +position = 'before' +match_indent = true +payload = ''' +if nc and area == G.pack_cards and not select_to then G.pack_cards:remove_card(card); G.consumeables:emplace(card) end''' + +# G.FUNC.use_card() +[[patches]] +[patches.pattern] +target = "functions/button_callbacks.lua" +pattern = '''if prev_state == G.STATES.TAROT_PACK then inc_career_stat('c_tarot_reading_used', 1) end''' +position = 'at' +match_indent = true +payload = '''if prev_state == SMODS.STATES.BOOSTER_OPENED and booster_obj.name:find('Arcana') then inc_career_stat('c_tarot_reading_used', 1) end''' + +# G.FUNC.use_card() +[[patches]] +[patches.pattern] +target = "functions/button_callbacks.lua" +pattern = '''if prev_state == G.STATES.PLANET_PACK then inc_career_stat('c_planetarium_used', 1) end''' +position = 'at' +match_indent = true +payload = '''if prev_state == SMODS.STATES.BOOSTER_OPENED and booster_obj.name:find('Celestial') then inc_career_stat('c_planetarium_used', 1) end''' + +# G.FUNC.use_card() +[[patches]] +[patches.pattern] +target = "functions/button_callbacks.lua" +pattern = "(G.STATE == G.STATES.BUFFOON_PACK and G.STATES.BUFFOON_PACK) or" +position = "before" +payload = "(G.STATE == SMODS.STATES.BOOSTER_OPENED and SMODS.STATES.BOOSTER_OPENED) or" +match_indent = true + +# G.FUNC.use_card() +[[patches]] +[patches.pattern] +target = "functions/state_events.lua" +pattern = "if not (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK) and" +position = "at" +payload = "if not (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED) and" +match_indent = true + +# Card:use_consumeable() +[[patches]] +[patches.regex] +target = "card.lua" +pattern = '''(?[\t ]*)align = \(G\.STATE[\s\S]*and -0\.2 or 0},''' +position = "at" +payload = ''' +align = (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED) and 'tm' or 'cm', +offset = {x = 0, y = (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED) and -0.2 or 0},''' +line_prepend = '$indent' + +# G.FUNCS.use_card() +[[patches]] +[patches.pattern] +target = "functions/button_callbacks.lua" +pattern = "e.config.ref_table:redeem()" +position = "before" +payload = "if area == G.pack_cards then e.config.ref_table.cost = 0 end" +match_indent = true + ## Stopping ease_dollars anim from playing when voucher is free # Card:redeem() [[patches]] @@ -173,6 +264,50 @@ if card.ability.consumeable and card.area == G.pack_cards and G.pack_cards and b end ''' +# G.FUNCS.use_card() +[[patches]] +[patches.pattern] +target = 'functions/button_callbacks.lua' +match_indent = true +position = 'at' +pattern = ''' +if card.ability.consumeable then + if nc then +''' +payload = ''' +if select_to then + card:add_to_deck() + G[select_to]:emplace(card) + if card.config.center.on_select and type(card.config.center.on_select) == 'function' then + card.config.center:on_select(card) + end + play_sound('card1', 0.8, 0.6) + play_sound('generic1') + dont_dissolve = true + delay_fac = 0.2 +elseif card.ability.consumeable then + if nc then +''' +# G.FUNCS.end_consumeable() +[[patches]] +[patches.pattern] +target = 'functions/button_callbacks.lua' +match_indent = true +position = 'before' +pattern = ''' +for i = 1, #G.GAME.tags do + if G.GAME.tags[i]:apply_to_run({type = 'new_blind_choice'}) then break end +end + +G.E_MANAGER:add_event(Event({trigger = 'after',delay = 0.2*delayfac, + func = function() + save_run() + return true + end})) +''' +payload = ''' +booster_obj = nil +''' # G.FUNCS.skip_booster() [[patches]] [patches.pattern] @@ -207,3 +342,18 @@ match_indent = true position = 'after' pattern = '''if self.shop_voucher then G.GAME.current_round.voucher = nil end''' payload = '''SMODS.enter_state(SMODS.STATES.REDEEM_VOUCHER, nil, {from_hold = true})''' + +# G.FUNCS.use_card() +# Consumables in areas other than the consumable don't count +# as picking an item in boosters when used +[[patches]] +[patches.pattern] +target = 'functions/button_callbacks.lua' +match_indent = true +position = 'at' +pattern = ''' +if area == G.consumeables then +''' +payload = ''' +if area ~= G.pack_cards then +''' diff --git a/lovely/center.toml b/lovely/center.toml index aa16b7a52..9ad3106d2 100644 --- a/lovely/center.toml +++ b/lovely/center.toml @@ -526,6 +526,58 @@ if obj and obj.remove_from_deck and type(obj.remove_from_deck) == 'function' the obj:remove_from_deck(self, from_debuff) end''' +# G.FUNCS.use_card() +[[patches]] +[patches.pattern] +target = 'functions/button_callbacks.lua' +match_indent = true +position = 'at' +pattern = ''' +if G.booster_pack and not G.booster_pack.alignment.offset.py and (card.ability.consumeable or not (G.GAME.pack_choices and G.GAME.pack_choices > 1)) then +''' +payload = ''' +local nc +local select_to = card.area == G.pack_cards and G.pack_cards and booster_obj and SMODS.card_select_area(card, booster_obj) and card:selectable_from_pack(booster_obj) +if card.ability.consumeable and not select_to then + local obj = card.config.center + if obj.keep_on_use and type(obj.keep_on_use) == 'function' then + nc = obj:keep_on_use(card) + end +end +if G.booster_pack and not G.booster_pack.alignment.offset.py and ((not select_to and card.ability.consumeable) or not (G.GAME.pack_choices and G.GAME.pack_choices > 1)) then + +''' + +# G.FUNCS.use_card() +[[patches]] +[patches.pattern] +target = 'functions/button_callbacks.lua' +pattern = "if card.area then card.area:remove_card(card) end" +match_indent = true +position = 'at' +payload = ''' +if not card.from_area then card.from_area = card.area end +if card.area and (not nc or card.area == G.pack_cards) then card.area:remove_card(card) end''' + +[[patches]] +[patches.pattern] +target = 'functions/button_callbacks.lua' +match_indent = true +position = 'before' +pattern = ''' +if area and area.cards[1] then +''' +payload = ''' +if nc and not area then G.consumeables:emplace(card) end +''' +[[patches]] +[patches.pattern] +target = 'functions/button_callbacks.lua' +pattern = "else draw_card(G.hand, G.play, 1, 'up', true, card, nil, mute) end" +match_indent = true +position = 'at' +payload = '''elseif not nc then draw_card(G.hand, G.play, 1, 'up', true, card, nil, mute) end''' + [[patches]] [patches.pattern] target = 'functions/button_callbacks.lua' diff --git a/src/game_objects/game_states.lua b/src/game_objects/game_states.lua index ffcf64cc5..7ec3accd0 100644 --- a/src/game_objects/game_states.lua +++ b/src/game_objects/game_states.lua @@ -426,7 +426,7 @@ SMODS.GameState { G:save_settings() end G.GAME.round_resets.blind = G.P_BLINDS[args.key] - G.GAME.round_resets.blind_states[G.GAME.blind_on_deck] = 'Current' -- TODO : Check this / G.GAME.blind_on_deck + G.GAME.round_resets.blind_states[G.GAME.blind_on_deck] = 'Current' delay(0.2) return true end})) From 7d8997c465f0ec9683708c54b03057f9d3b2e861 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Sat, 4 Apr 2026 23:40:40 +0200 Subject: [PATCH 14/39] Added `G.FUNCS.use_card()` `GameState` patches +Title, hopefully that's all... (..hyuck) --- lovely/game_state.toml | 70 ++++++++++++++++ src/overrides.lua | 185 ----------------------------------------- 2 files changed, 70 insertions(+), 185 deletions(-) diff --git a/lovely/game_state.toml b/lovely/game_state.toml index b0739526c..606e806ef 100644 --- a/lovely/game_state.toml +++ b/lovely/game_state.toml @@ -54,3 +54,73 @@ pattern = '''(self.config.type == 'hand' and ({[G.STATES.SHOP]=1, [G.STATES.TARO position = 'at' match_indent = true payload = '''(self.config.type == 'hand' and ({[SMODS.STATES.SHOP]=1, [G.STATES.TAROT_PACK]=1, [G.STATES.SPECTRAL_PACK]=1, [G.STATES.STANDARD_PACK]=1,[G.STATES.BUFFOON_PACK]=1,[G.STATES.PLANET_PACK]=1, [SMODS.STATES.ROUND_EVAL]=1, [SMODS.STATES.BLIND_SELECT]=1})[state]) or''' + +# functions/button_callbacks.lua : G.FUNCS.use_card() : update logic +[[patches]] +[patches.pattern] +target = 'functions/button_callbacks.lua' +match_indent = true +position = 'at' +pattern = ''' +if card.ability.set == 'Booster' and not nosave and G.STATE == G.STATES.SHOP +''' +payload = ''' +if card.ability.set == 'Booster' and not nosave and G.STATE == SMODS.STATES.SHOP +''' +[[patches]] +[patches.pattern] +target = 'functions/button_callbacks.lua' +match_indent = true +position = 'at' +pattern = ''' +if G.shop and not G.shop.alignment.offset.py then + G.shop.alignment.offset.py = G.shop.alignment.offset.y + G.shop.alignment.offset.y = G.ROOM.T.y + 29 +end +if G.blind_select and not G.blind_select.alignment.offset.py then + G.blind_select.alignment.offset.py = G.blind_select.alignment.offset.y + G.blind_select.alignment.offset.y = G.ROOM.T.y + 39 +end +if G.round_eval and not G.round_eval.alignment.offset.py then + G.round_eval.alignment.offset.py = G.round_eval.alignment.offset.y + G.round_eval.alignment.offset.y = G.ROOM.T.y + 29 +end +''' +payload = ''' + +''' +[[patches]] +[patches.pattern] +target = 'functions/button_callbacks.lua' +match_indent = true +position = 'at' +pattern = ''' +if G.shop then + G.shop.alignment.offset.y = G.shop.alignment.offset.py + G.shop.alignment.offset.py = nil +end +if G.blind_select then + G.blind_select.alignment.offset.y = G.blind_select.alignment.offset.py + G.blind_select.alignment.offset.py = nil +end +if G.round_eval then + G.round_eval.alignment.offset.y = G.round_eval.alignment.offset.py + G.round_eval.alignment.offset.py = nil +end +''' +payload = ''' + +''' +[[patches]] +[patches.pattern] +target = 'functions/button_callbacks.lua' +match_indent = true +position = 'at' +pattern = '''G.STATE = prev_state''' +payload = ''' +if SMODS.GameStates[G.STATE] and SMODS.GameStates[G.STATE].exit_after_use_card then -- SMODS.STATES.BOOSTER_OPENED is handled by G.FUNCS.end_consumeable() below + SMODS.exit_state(nil, nil, {state_override=prev_state}) +elseif not SMODS.GameStates[G.STATE] then + G.STATE = prev_state +end +''' \ No newline at end of file diff --git a/src/overrides.lua b/src/overrides.lua index d86c8f6e5..181606ae4 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -3028,191 +3028,6 @@ G.FUNCS.evaluate_round = function() add_round_eval_row({name = 'bottom', dollars = dollars}) end --- Todo : *maybe* turn this into a lovely patch -G.FUNCS.use_card = function(e, mute, nosave) - e.config.button = nil - local card = e.config.ref_table - local area = card.area - local prev_state = G.STATE - local dont_dissolve = nil - local delay_fac = 1 - - if card:check_use() then - G.E_MANAGER:add_event(Event({ - func = function() - e.disable_button = nil - e.config.button = 'use_card' - return true end - })) - return - end - - if card.ability.set == 'Booster' and not nosave and G.STATE == SMODS.STATES.SHOP then - save_with_action({ - type = 'use_card', - card = card.sort_id, - }) - end - - G.TAROT_INTERRUPT = G.STATE - if card.ability.set == 'Booster' then G.GAME.PACK_INTERRUPT = G.STATE end - G.STATE = (G.STATE == G.STATES.TAROT_PACK and G.STATES.TAROT_PACK) or - (G.STATE == G.STATES.PLANET_PACK and G.STATES.PLANET_PACK) or - (G.STATE == G.STATES.SPECTRAL_PACK and G.STATES.SPECTRAL_PACK) or - (G.STATE == G.STATES.STANDARD_PACK and G.STATES.STANDARD_PACK) or - (G.STATE == SMODS.STATES.BOOSTER_OPENED and SMODS.STATES.BOOSTER_OPENED) or - (G.STATE == G.STATES.BUFFOON_PACK and G.STATES.BUFFOON_PACK) or - G.STATES.PLAY_TAROT - - G.CONTROLLER.locks.use = true - local nc - local select_to = card.area == G.pack_cards and G.pack_cards and booster_obj and SMODS.card_select_area(card, booster_obj) and card:selectable_from_pack(booster_obj) - if card.ability.consumeable and not select_to then - local obj = card.config.center - if obj.keep_on_use and type(obj.keep_on_use) == 'function' then - nc = obj:keep_on_use(card) - end - end - if G.booster_pack and not G.booster_pack.alignment.offset.py and ((not select_to and card.ability.consumeable) or not (G.GAME.pack_choices and G.GAME.pack_choices > 1)) then - G.booster_pack.alignment.offset.py = G.booster_pack.alignment.offset.y - G.booster_pack.alignment.offset.y = G.ROOM.T.y + 29 - end - - if card.children.use_button then card.children.use_button:remove(); card.children.use_button = nil end - if card.children.sell_button then card.children.sell_button:remove(); card.children.sell_button = nil end - if card.children.price then card.children.price:remove(); card.children.price = nil end - - if not card.from_area then card.from_area = card.area end - if card.area and (not nc or card.area == G.pack_cards) then card.area:remove_card(card) end - - if select_to then - card:add_to_deck() - G[select_to]:emplace(card) - if card.config.center.on_select and type(card.config.center.on_select) == 'function' then - card.config.center:on_select(card) - end - play_sound('card1', 0.8, 0.6) - play_sound('generic1') - dont_dissolve = true - delay_fac = 0.2 - elseif card.ability.consumeable then - if nc then - if area then area:remove_from_highlighted(card) end - play_sound('cardSlide2', nil, 0.3) - dont_dissolve = true - end - if (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED) then - card.T.x = G.hand.T.x + G.hand.T.w/2 - card.T.w/2 - card.T.y = G.hand.T.y + G.hand.T.h/2 - card.T.h/2 - 0.5 - discover_card(card.config.center) - elseif not nc then draw_card(G.hand, G.play, 1, 'up', true, card, nil, mute) end - delay(0.2) - e.config.ref_table:use_consumeable(area) - SMODS.calculate_context({using_consumeable = true, consumeable = card, area = card.from_area}) - elseif card.ability.set == 'Enhanced' or card.ability.set == 'Default' then - G.playing_card = (G.playing_card and G.playing_card + 1) or 1 - G.deck:emplace(card) - play_sound('card1', 0.8, 0.6) - play_sound('generic1') - card.playing_card = G.playing_card - playing_card_joker_effects({card}) - card:add_to_deck() - table.insert(G.playing_cards, card) - dont_dissolve = true - delay_fac = 0.2 - elseif card.ability.set == 'Joker' then - card:add_to_deck() - G.jokers:emplace(card) - play_sound('card1', 0.8, 0.6) - play_sound('generic1') - dont_dissolve = true - delay_fac = 0.2 - elseif card.ability.set == 'Booster' then - delay(0.1) - if card.ability.booster_pos then G.GAME.current_round.used_packs[card.ability.booster_pos] = 'USED' end - draw_card(G.hand, G.play, 1, 'up', true, card, nil, true) - if not card.from_tag then - G.GAME.round_scores.cards_purchased.amt = G.GAME.round_scores.cards_purchased.amt + 1 - end - e.config.ref_table:open() - elseif card.ability.set == 'Voucher' then - delay(0.1) - draw_card(G.hand, G.play, 1, 'up', true, card, nil, true) - G.GAME.round_scores.cards_purchased.amt = G.GAME.round_scores.cards_purchased.amt + 1 - if area == G.pack_cards then e.config.ref_table.cost = 0 end - e.config.ref_table:redeem() - end - if card.ability.set == 'Booster' then - G.CONTROLLER.locks.use = false - G.TAROT_INTERRUPT = nil - else - G.E_MANAGER:add_event(Event({ - trigger = 'after', - delay = 0.2, - func = function() - if not dont_dissolve then card:start_dissolve() end - G.E_MANAGER:add_event(Event({ - trigger = 'after', - delay = 0.1, - func = function() - if SMODS.GameStates[G.STATE] and SMODS.GameStates[G.STATE].exit_after_use_card then -- SMODS.STATES.BOOSTER_OPENED is handled by G.FUNCS.end_consumeable() below - SMODS.exit_state(nil, nil, {state_override=prev_state}) - elseif not SMODS.GameStates[G.STATE] then - G.STATE = prev_state - end - G.TAROT_INTERRUPT=nil - G.CONTROLLER.locks.use = false - - if (prev_state == G.STATES.TAROT_PACK or prev_state == G.STATES.PLANET_PACK or - prev_state == G.STATES.SPECTRAL_PACK or prev_state == G.STATES.STANDARD_PACK or - prev_state == SMODS.STATES.BOOSTER_OPENED or - prev_state == G.STATES.BUFFOON_PACK) and G.booster_pack then - if nc and area == G.pack_cards and not select_to then G.pack_cards:remove_card(card); G.consumeables:emplace(card) end - if area ~= G.pack_cards then - G.booster_pack.alignment.offset.y = G.booster_pack.alignment.offset.py - G.booster_pack.alignment.offset.py = nil - elseif G.GAME.pack_choices and G.GAME.pack_choices > 1 then - if G.booster_pack.alignment.offset.py then - G.booster_pack.alignment.offset.y = G.booster_pack.alignment.offset.py - G.booster_pack.alignment.offset.py = nil - end - G.GAME.pack_choices = G.GAME.pack_choices - 1 - else - G.CONTROLLER.interrupt.focus = true - if prev_state == SMODS.STATES.BOOSTER_OPENED and booster_obj.name:find('Arcana') then inc_career_stat('c_tarot_reading_used', 1) end - if prev_state == SMODS.STATES.BOOSTER_OPENED and booster_obj.name:find('Celestial') then inc_career_stat('c_planetarium_used', 1) end - G.FUNCS.end_consumeable(nil, delay_fac) - end - else - if nc and not area then G.consumeables:emplace(card) end - if area and area.cards[1] then - G.E_MANAGER:add_event(Event({ - func = function() - G.E_MANAGER:add_event(Event({ - func = function() - G.CONTROLLER.interrupt.focus = nil - if card.ability.set == 'Voucher' then - G.CONTROLLER:snap_to({node = G.shop:get_UIE_by_ID('next_round_button')}) - elseif area then - G.CONTROLLER:recall_cardarea_focus(area) - end - return true - end - })) - return true - end - })) - end - end - return true - end - })) - return true - end - })) - end -end - -- Todo : turn this into a lovely patch G.FUNCS.end_consumeable = function(e, delayfac) delayfac = delayfac or 1 From c695f38a312269db83b317aef6f02f887a1b7cf2 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Sat, 4 Apr 2026 23:49:14 +0200 Subject: [PATCH 15/39] Removed `G.FUNCS.end_consumeable()` override in favor of patches -/*Title --- lovely/game_state.toml | 37 +++++++++++++++++++++++ src/overrides.lua | 66 ------------------------------------------ 2 files changed, 37 insertions(+), 66 deletions(-) diff --git a/lovely/game_state.toml b/lovely/game_state.toml index 606e806ef..b4568d372 100644 --- a/lovely/game_state.toml +++ b/lovely/game_state.toml @@ -123,4 +123,41 @@ if SMODS.GameStates[G.STATE] and SMODS.GameStates[G.STATE].exit_after_use_card t elseif not SMODS.GameStates[G.STATE] then G.STATE = prev_state end +''' + +# functions/button_callbacks.lua : G.FUNCS.end_consumeable() : update logic +[[patches]] +[patches.pattern] +target = 'functions/button_callbacks.lua' +match_indent = true +position = 'at' +pattern = ''' +if G.shop and G.shop.alignment.offset.py then + G.shop.alignment.offset.y = G.shop.alignment.offset.py + G.shop.alignment.offset.py = nil +end +if G.blind_select and G.blind_select.alignment.offset.py then + G.blind_select.alignment.offset.y = G.blind_select.alignment.offset.py + G.blind_select.alignment.offset.py = nil +end +if G.round_eval and G.round_eval.alignment.offset.py then + G.round_eval.alignment.offset.y = G.round_eval.alignment.offset.py + G.round_eval.alignment.offset.py = nil +end +''' +payload = ''' + +''' +[[patches]] +[patches.pattern] +target = 'functions/button_callbacks.lua' +match_indent = true +position = 'at' +pattern = '''G.STATE = G.GAME.PACK_INTERRUPT''' +payload = ''' +if SMODS.GameStates[G.STATE] and SMODS.GameStates[G.STATE].exit_after_end_consumable then + SMODS.exit_state(nil, nil, {state_override=G.GAME.PACK_INTERRUPT}) +elseif not SMODS.GameStates[G.STATE] then + G.STATE = G.GAME.PACK_INTERRUPT +end ''' \ No newline at end of file diff --git a/src/overrides.lua b/src/overrides.lua index 181606ae4..86ef1980b 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -3026,70 +3026,4 @@ G.FUNCS.evaluate_round = function() })) end add_round_eval_row({name = 'bottom', dollars = dollars}) -end - --- Todo : turn this into a lovely patch -G.FUNCS.end_consumeable = function(e, delayfac) - delayfac = delayfac or 1 - stop_use() - if G.booster_pack then - if G.booster_pack_sparkles then G.booster_pack_sparkles:fade(1*delayfac) end - if G.booster_pack_stars then G.booster_pack_stars:fade(1*delayfac) end - if G.booster_pack_meteors then G.booster_pack_meteors:fade(1*delayfac) end - G.booster_pack.alignment.offset.y = G.ROOM.T.y + 9 - - G.E_MANAGER:add_event(Event({trigger = 'after',delay = 0.2*delayfac,blocking = false, blockable = false, - func = function() - G.booster_pack:remove() - G.booster_pack = nil - return true - end})) - G.E_MANAGER:add_event(Event({trigger = 'after',delay = 1*delayfac,blocking = false, blockable = false, - func = function() - if G.booster_pack_sparkles then G.booster_pack_sparkles:remove(); G.booster_pack_sparkles = nil end - if G.booster_pack_stars then G.booster_pack_stars:remove(); G.booster_pack_stars = nil end - if G.booster_pack_meteors then G.booster_pack_meteors:remove(); G.booster_pack_meteors = nil end - return true - end})) - end - - delay(0.2*delayfac) - G.E_MANAGER:add_event(Event({ - trigger = 'after', - delay = 0.2*delayfac, - func = function() - G.FUNCS.draw_from_hand_to_deck() - G.E_MANAGER:add_event(Event({ - trigger = 'after', - delay = 0.2*delayfac, - func = function() - G.CONTROLLER.interrupt.focus = true - G.E_MANAGER:add_event(Event({func = function() - if G.shop then G.CONTROLLER:snap_to({node = G.shop:get_UIE_by_ID('next_round_button')}) end - return true end })) - if SMODS.GameStates[G.STATE] and SMODS.GameStates[G.STATE].exit_after_end_consumable then - SMODS.exit_state(nil, nil, {state_override=G.GAME.PACK_INTERRUPT}) - elseif not SMODS.GameStates[G.STATE] then - G.STATE = G.GAME.PACK_INTERRUPT - end - ease_background_colour_blind(G.GAME.PACK_INTERRUPT) - G.GAME.PACK_INTERRUPT = nil - return true - end - })) - SMODS.calculate_context({ending_booster = true, booster = booster_obj}) - booster_obj = nil - for i = 1, #G.GAME.tags do - if G.GAME.tags[i]:apply_to_run({type = 'new_blind_choice'}) then break end - end - - G.E_MANAGER:add_event(Event({trigger = 'after',delay = 0.2*delayfac, - func = function() - save_run() - return true - end})) - - return true - end - })) end \ No newline at end of file From fa584de76b5a7d38aca5239bee000184536b8e7e Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Mon, 6 Apr 2026 23:09:46 +0200 Subject: [PATCH 16/39] Added more `SMODS.state_stack` utility functions + polished `from_hold` behaviour a little +/*Title, the new functions are mostly for internal use, and `from_hold` may still not be fully implemented for all states, especially `BLIND`. --- src/game_objects/game_states.lua | 109 ++++++++++++++++++++++++++----- 1 file changed, 94 insertions(+), 15 deletions(-) diff --git a/src/game_objects/game_states.lua b/src/game_objects/game_states.lua index 7ec3accd0..563a98a09 100644 --- a/src/game_objects/game_states.lua +++ b/src/game_objects/game_states.lua @@ -18,11 +18,40 @@ function SMODS.push_to_state_stack(state, args) table.insert(SMODS.state_stack, {state=state, args=args}) end -function SMODS.pop_from_state_stack(state) +function SMODS.pop_from_state_stack(state, pop_duplicate) if #SMODS.state_stack < 1 then return end - if SMODS.state_stack[#SMODS.state_stack].state == state then - table.remove(SMODS.state_stack, #SMODS.state_stack) + pop_duplicate = pop_duplicate == nil or pop_duplicate + local index = #SMODS.state_stack + while index > 0 and SMODS.state_stack[index].state == state do + table.remove(SMODS.state_stack, index) + index = index - 1 + if not pop_duplicate then + index = 0 + end + end +end + +function SMODS.get_previous_state(allow_duplicate) + local index = #SMODS.state_stack - 1 + while index > 0 and SMODS.state_stack[index].state ~= SMODS.STATE do + index = index - 1 + if allow_duplicate or SMODS.state_stack[index].state ~= SMODS.STATE then + return SMODS.state_stack[index].state + end + end + return nil +end + +function SMODS.is_state_in_stack(state, exclude_latest) + for i, state_table in ipairs(SMODS.state_stack) do + if exclude_latest and i == #SMODS.state_stack then + return false + end + if state_table.state == state then + return true + end end + return false end function SMODS.clear_state_stack() @@ -33,11 +62,21 @@ function SMODS.enter_state(new_state, enter_args, exit_args) local current_state = SMODS.STATE or G.STATE -- It only handles on_exit() and on_enter() for SMODS.GameState states if current_state == new_state then return end enter_args = enter_args or {} + enter_args.old_state = current_state + if SMODS.is_state_in_stack(new_state) then -- If the new_state is currently held in the stack + if not enter_args.force_refresh then + enter_args.from_hold = true + end + end exit_args = exit_args or {} exit_args.new_state = exit_args.new_state or new_state + local exit_state_in_stack = SMODS.is_state_in_stack(current_state, true) + if exit_state_in_stack then + exit_args.from_hold = true + end if SMODS.GameStates[current_state] then SMODS.GameStates[current_state]:on_exit(exit_args) - if not exit_args.from_hold then + if not exit_args.from_hold or exit_state_in_stack then SMODS.pop_from_state_stack(current_state) end end @@ -52,17 +91,25 @@ end function SMODS.exit_state(exit_args, enter_args, default) local current_state = SMODS.STATE or G.STATE local new_state - if #SMODS.state_stack < 2 then + default = default or {} + default.enter_args = default.enter_args or {} + if #SMODS.state_stack < 2 or not SMODS.get_previous_state() then new_state = default.state_override or SMODS.default_state else new_state = SMODS.state_stack[#SMODS.state_stack - 1].state end exit_args = exit_args or {} exit_args.new_state = exit_args.new_state or new_state + if SMODS.is_state_in_stack(current_state, true) then + exit_args.from_hold = true + end enter_args = enter_args or {} - enter_args.from_hold = true - default = default or {} - default.enter_args = default.enter_args or {} + enter_args.old_state = current_state + if SMODS.is_state_in_stack(new_state) then + if not enter_args.force_refresh then + enter_args.from_hold = true + end + end if SMODS.GameStates[current_state] then SMODS.GameStates[current_state]:on_exit(exit_args) SMODS.pop_from_state_stack(current_state) @@ -123,13 +170,35 @@ SMODS.GameState { SMODS.GameState { key = SMODS.STATES.SHOP, on_enter = function (self, args) - if args.from_hold then - -- Extracted from G.FUNCS.use_card() + if args.force_refresh then + self:on_exit({}) + elseif args.from_hold then -- Todo : Store data to and restore data from SMODS.state_stack if G.shop then + -- Extracted from G.FUNCS.use_card() G.shop.alignment.offset.y = G.shop.alignment.offset.py G.shop.alignment.offset.py = nil G.SHOP_SIGN.alignment.offset.y = 0 + + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 0.2, + blockable = false, + func = function() + if math.abs(G.shop.T.y - G.shop.VT.y) < 3 then + G.ROOM.jiggle = G.ROOM.jiggle + 3 + play_sound('cardFan2') + for i = 1, #G.GAME.tags do + G.GAME.tags[i]:apply_to_run({type = 'shop_start'}) + end + return true + end + end + })) + + SMODS.calculate_context({starting_shop = true, from_hold = true}) + G.CONTROLLER:snap_to({node = G.shop:get_UIE_by_ID('next_round_button')}) + G.E_MANAGER:add_event(Event({ func = function() save_run(); return true end})) end return end @@ -258,6 +327,7 @@ SMODS.GameState { G.shop.alignment.offset.py = G.shop.alignment.offset.y G.shop.alignment.offset.y = G.ROOM.T.y + 29 G.SHOP_SIGN.alignment.offset.y = -15 + delay(0.5) end return end @@ -307,7 +377,9 @@ SMODS.GameState { SMODS.GameState { key = SMODS.STATES.ROUND_EVAL, on_enter = function (self, args) - if args.from_hold then + if args.force_refresh then + self:on_exit({}) + elseif args.from_hold then if G.round_eval then G.round_eval.alignment.offset.y = G.round_eval.alignment.offset.py G.round_eval.alignment.offset.py = nil @@ -405,7 +477,9 @@ SMODS.GameState { SMODS.GameState { key = SMODS.STATES.BLIND, on_enter = function (self, args) - if args.from_hold then + if args.force_refresh then + self:on_exit({}) + elseif args.from_hold then -- Todo: Implement return end @@ -414,7 +488,6 @@ SMODS.GameState { func = function () stop_use() G.GAME.facing_blind = true - G.E_MANAGER:add_event(Event({ trigger = 'immediate', func = function() @@ -455,7 +528,7 @@ SMODS.GameState { trigger = "immediate", blocking = false, func = function () - if args.new_state == SMODS.STATES.ROUND_EVAL then -- Precise vanilla timing + if args.new_state == SMODS.STATES.ROUND_EVAL then -- Precise vanilla timing -> called from end_round() if G.round_eval and G.round_eval.alignment.offset.y == -7.8 and math.abs(G.round_eval.T.y - G.round_eval.VT.y) < 3 then G.E_MANAGER:add_event(Event({ trigger = "immediate", @@ -486,6 +559,10 @@ SMODS.GameState { return true end })) + for k, v in ipairs(G.playing_cards) do + v.ability.discarded = nil + v.ability.forced_selection = nil + end delay(0.4) return true end @@ -513,7 +590,9 @@ SMODS.GameState { SMODS.GameState { key = SMODS.STATES.BLIND_SELECT, on_enter = function (self, args) - if args.from_hold then + if args.force_refresh then + self:on_exit({}) + elseif args.from_hold then if G.blind_select then G.blind_select.alignment.offset.y = G.blind_select.alignment.offset.py G.blind_select.alignment.offset.py = nil From 0432c5bd014b702be4613551c7f7828d368d3496 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Mon, 6 Apr 2026 23:12:26 +0200 Subject: [PATCH 17/39] Minor refactor + fix *Title, better name for what it does + fixes an oversight. --- src/game_objects/game_states.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/game_objects/game_states.lua b/src/game_objects/game_states.lua index 563a98a09..b240ae685 100644 --- a/src/game_objects/game_states.lua +++ b/src/game_objects/game_states.lua @@ -31,7 +31,7 @@ function SMODS.pop_from_state_stack(state, pop_duplicate) end end -function SMODS.get_previous_state(allow_duplicate) +function SMODS.get_next_held_state(allow_duplicate) local index = #SMODS.state_stack - 1 while index > 0 and SMODS.state_stack[index].state ~= SMODS.STATE do index = index - 1 @@ -91,12 +91,13 @@ end function SMODS.exit_state(exit_args, enter_args, default) local current_state = SMODS.STATE or G.STATE local new_state + local next_held_state = SMODS.get_next_held_state() default = default or {} default.enter_args = default.enter_args or {} - if #SMODS.state_stack < 2 or not SMODS.get_previous_state() then + if #SMODS.state_stack < 2 or not next_held_state then new_state = default.state_override or SMODS.default_state else - new_state = SMODS.state_stack[#SMODS.state_stack - 1].state + new_state = next_held_state end exit_args = exit_args or {} exit_args.new_state = exit_args.new_state or new_state From c6eaeb4fbe914ff9d84535e40dbc76235e6e7009 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Mon, 6 Apr 2026 23:14:54 +0200 Subject: [PATCH 18/39] Minor refactor^2 *Title, realized this could be simpler --- src/game_objects/game_states.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/game_objects/game_states.lua b/src/game_objects/game_states.lua index b240ae685..d2ba9dec1 100644 --- a/src/game_objects/game_states.lua +++ b/src/game_objects/game_states.lua @@ -94,10 +94,10 @@ function SMODS.exit_state(exit_args, enter_args, default) local next_held_state = SMODS.get_next_held_state() default = default or {} default.enter_args = default.enter_args or {} - if #SMODS.state_stack < 2 or not next_held_state then - new_state = default.state_override or SMODS.default_state - else + if next_held_state then new_state = next_held_state + else + new_state = default.state_override or SMODS.default_state end exit_args = exit_args or {} exit_args.new_state = exit_args.new_state or new_state From 2da4e7845f585c26d1cfac4c1d9f832c0fbfbe08 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Tue, 7 Apr 2026 00:12:45 +0200 Subject: [PATCH 19/39] Added `SMODS.state_queue` +/*Title, this replaces straight `SMODS.enter_state()` calls to allow queueing states instead. -> Base SMODS only queues the next state once it would've called `SMODS.enter_state()`, and only if no states are currently queued, as if there is a state queued, it's best if the mod that queued it also handles advancing the state queue after. --- src/game_objects/game_states.lua | 26 ++++++++++++++++++++++++++ src/overrides.lua | 20 ++++++++++++++++---- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/game_objects/game_states.lua b/src/game_objects/game_states.lua index d2ba9dec1..cb06e4878 100644 --- a/src/game_objects/game_states.lua +++ b/src/game_objects/game_states.lua @@ -128,6 +128,32 @@ function SMODS.exit_state(exit_args, enter_args, default) end end +SMODS.state_queue = {} + +function SMODS.queue_state(new_state, enter_args, exit_args) + SMODS.state_queue[#SMODS.state_queue+1] = {state = new_state, enter_args = enter_args, exit_args = exit_args} +end + +function SMODS.advance_state_queue(instant) + if #SMODS.state_queue < 1 then + return + end + local func = function() + local queue_table = table.remove(SMODS.state_queue, 1) + SMODS.enter_state(queue_table.state, queue_table.enter_args, queue_table.exit_args) + return true + end + if instant then + func() + else + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + func = func + })) + end +end + + local delete_run_ref = Game.delete_run function Game:delete_run() local ret = delete_run_ref(self) diff --git a/src/overrides.lua b/src/overrides.lua index 86ef1980b..4faab1f21 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -2725,7 +2725,10 @@ end -- SMODS.GameState related overrides function G.FUNCS.toggle_shop(e) - SMODS.enter_state(SMODS.STATES.BLIND_SELECT) + if #SMODS.state_queue == 0 then + SMODS.queue_state(SMODS.STATES.BLIND_SELECT) + end + SMODS.advance_state_queue() end function G.FUNCS.cash_out(e) @@ -2735,7 +2738,10 @@ function G.FUNCS.cash_out(e) G.E_MANAGER:add_event(Event({ trigger = 'immediate', func = function() - SMODS.enter_state(SMODS.STATES.SHOP) + if #SMODS.state_queue == 0 then + SMODS.queue_state(SMODS.STATES.SHOP) + end + SMODS.advance_state_queue() G.STATE_COMPLETE = false return true end @@ -2746,7 +2752,10 @@ end function G.FUNCS.select_blind(e) stop_use() if G.blind_select then - SMODS.enter_state(SMODS.STATES.BLIND, {key = e.config.ref_table.key}) + if #SMODS.state_queue == 0 then + SMODS.queue_state(SMODS.STATES.BLIND, {key = e.config.ref_table.key}) + end + SMODS.advance_state_queue() end end @@ -2862,7 +2871,10 @@ function end_round() trigger = 'after', delay = 0.3, func = function() - SMODS.enter_state(SMODS.STATES.ROUND_EVAL) + if #SMODS.state_queue == 0 then + SMODS.queue_state(SMODS.STATES.ROUND_EVAL) + end + SMODS.advance_state_queue() G.STATE_COMPLETE = false if G.GAME.round_resets.blind == G.P_BLINDS.bl_small then From dc400d19684915cc07cf0b2deaaf678bca5d049b Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Tue, 7 Apr 2026 00:26:14 +0200 Subject: [PATCH 20/39] Added missing clean-up function + refactored +/*Title --- src/game_objects/game_states.lua | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/game_objects/game_states.lua b/src/game_objects/game_states.lua index cb06e4878..7276d100a 100644 --- a/src/game_objects/game_states.lua +++ b/src/game_objects/game_states.lua @@ -153,12 +153,21 @@ function SMODS.advance_state_queue(instant) end end +function SMODS.clear_state_queue() + SMODS.state_queue = {} +end + + +local _delete_run_clear_state_stuff = function () + SMODS.STATE = nil + SMODS.clear_state_stack() + SMODS.clear_state_queue() +end local delete_run_ref = Game.delete_run function Game:delete_run() local ret = delete_run_ref(self) - SMODS.STATE = nil - SMODS.clear_state_stack() + _delete_run_clear_state_stuff() return ret end From 8fc368ac87279a663d213c5c03ab437aafbd3ca4 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Tue, 7 Apr 2026 00:40:42 +0200 Subject: [PATCH 21/39] Removed relic from AntePath PR -Title --- src/game_objects/game_states.lua | 15 ++++++--------- src/overrides.lua | 10 ++++------ 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/game_objects/game_states.lua b/src/game_objects/game_states.lua index 7276d100a..f7e78868e 100644 --- a/src/game_objects/game_states.lua +++ b/src/game_objects/game_states.lua @@ -59,11 +59,11 @@ function SMODS.clear_state_stack() end function SMODS.enter_state(new_state, enter_args, exit_args) - local current_state = SMODS.STATE or G.STATE -- It only handles on_exit() and on_enter() for SMODS.GameState states + local current_state = SMODS.STATE or G.STATE if current_state == new_state then return end enter_args = enter_args or {} enter_args.old_state = current_state - if SMODS.is_state_in_stack(new_state) then -- If the new_state is currently held in the stack + if SMODS.is_state_in_stack(new_state) then if not enter_args.force_refresh then enter_args.from_hold = true end @@ -158,16 +158,16 @@ function SMODS.clear_state_queue() end -local _delete_run_clear_state_stuff = function () +local delete_run_clear_state_stuff = function () SMODS.STATE = nil SMODS.clear_state_stack() SMODS.clear_state_queue() end -local delete_run_ref = Game.delete_run +local _delete_run_ref = Game.delete_run function Game:delete_run() - local ret = delete_run_ref(self) - _delete_run_clear_state_stuff() + local ret = _delete_run_ref(self) + delete_run_clear_state_stuff() return ret end @@ -407,7 +407,6 @@ SMODS.GameState { })) end end, - check_win = true, } SMODS.GameState { @@ -507,7 +506,6 @@ SMODS.GameState { reset_blinds() delay(0.6) end, - check_win = true, } SMODS.GameState { @@ -702,5 +700,4 @@ SMODS.GameState { })) end, - check_win = true, } \ No newline at end of file diff --git a/src/overrides.lua b/src/overrides.lua index 4faab1f21..b3b8b03ed 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -2783,7 +2783,7 @@ function end_round() -- context.end_of_round calculations SMODS.saved = false G.GAME.saved_text = nil - SMODS.calculate_context({end_of_round = true, game_over = game_over, beat_boss = G.GAME.blind.boss }) + SMODS.calculate_context({end_of_round = true, game_over = game_over, beat_boss = G.GAME.blind:is_type("Boss") }) if SMODS.saved then game_over = false end -- TARGET: main end_of_round evaluation if G.GAME.round_resets.ante == G.GAME.win_ante and G.GAME.blind:is_type("Boss") then @@ -2838,11 +2838,9 @@ function end_round() blocking = false, blockable = false, func = (function() - if SMODS.GameStates[G.STATE] and SMODS.GameStates[G.STATE].check_win then - win_game() - G.GAME.won = true - return true - end + win_game() + G.GAME.won = true + return true end) })) end From d6480094d61a9bb5d8872542513b096805b9d698 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Tue, 7 Apr 2026 00:52:12 +0200 Subject: [PATCH 22/39] Fix crash *Title, woopsie --- src/game_objects/game_states.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game_objects/game_states.lua b/src/game_objects/game_states.lua index f7e78868e..a6b1c56e0 100644 --- a/src/game_objects/game_states.lua +++ b/src/game_objects/game_states.lua @@ -34,10 +34,10 @@ end function SMODS.get_next_held_state(allow_duplicate) local index = #SMODS.state_stack - 1 while index > 0 and SMODS.state_stack[index].state ~= SMODS.STATE do - index = index - 1 if allow_duplicate or SMODS.state_stack[index].state ~= SMODS.STATE then return SMODS.state_stack[index].state end + index = index - 1 end return nil end From 9a3cc4de8450642f39e75179da34509c620a1a97 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Tue, 7 Apr 2026 00:54:13 +0200 Subject: [PATCH 23/39] Removed unnecessary override -Title --- src/game_object.lua | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/game_object.lua b/src/game_object.lua index 45254961e..d83ea0562 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -1833,28 +1833,6 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. return self.blind_types end } - --[[ - blind_states and loc_blind_states - - Game:update(dt) -> This; - if G.prev_small_state ~= G.GAME.round_resets.blind_states.Small or - G.prev_large_state ~= G.GAME.round_resets.blind_states.Big or - G.prev_boss_state ~= G.GAME.round_resets.blind_states.Boss or G.RESET_BLIND_STATES then ... - can probably be ignored and replaced with own system - - Game:start_run() -> get_next_tag_key() is called, besides that; blind_states is set - ]] - function reset_blinds() - G.GAME.round_resets.blind_states = G.GAME.round_resets.blind_states or {Small = 'Select', Big = 'Upcoming', Boss = 'Upcoming'} - if G.GAME.round_resets.blind_states.Boss == 'Defeated' then - G.GAME.round_resets.blind_states.Small = 'Upcoming' - G.GAME.round_resets.blind_states.Big = 'Upcoming' - G.GAME.round_resets.blind_states.Boss = 'Upcoming' - G.GAME.blind_on_deck = 'Small' - G.GAME.round_resets.blind_choices.Boss = SMODS.get_new_blind({Boss = true}) - G.GAME.round_resets.boss_rerolled = false - end - end function SMODS.get_blind_types(blind_obj) local ret From 9be6b7eac560b89f740a8449cbb3021fb7280f29 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Tue, 7 Apr 2026 13:47:27 +0200 Subject: [PATCH 24/39] Fixed Showdown Blinds crashing / being missing *Title --- src/game_object.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/game_object.lua b/src/game_object.lua index d83ea0562..9f560e874 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -1859,7 +1859,8 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. function get_new_boss() -- sendWarnMessage("get_new_boss() is deprecated; Call SMODS.get_new_blind() instead.", "utils") - local boss = SMODS.get_new_blind({Boss = true}) + local b_types = G.GAME.round_resets.ante % G.GAME.win_ante == 0 and {Showdown = true} or {Boss = true} + local boss = SMODS.get_new_blind(b_types) return boss end @@ -1884,7 +1885,7 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. local b_types = SMODS.get_blind_types(v) for b_type, _ in pairs(b_types) do if blind_types[b_type] then - if v.in_pool or not v.boss or (v.boss.min <= math.max(1, G.GAME.round_resets.ante) and ((math.max(1, G.GAME.round_resets.ante))%G.GAME.win_ante ~= 0 or G.GAME.round_resets.ante < 2)) then + if v.in_pool or not (v.boss and v.boss.min and v.boss.min > math.max(1, G.GAME.round_resets.ante)) then eligible_bosses[k] = true break end From d2c2bbd95bca7bd5e0620aefd7b8fec43af83269 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Tue, 7 Apr 2026 17:52:00 +0200 Subject: [PATCH 25/39] Implemented holding `SMODS.STATES.BLIND` + saving and loading the `SMODS.STATE` related stuff +Title, saving and loading are yet untested though. -> Currently no state stores unserializable data, in fact only `BLIND` stores any data at all; The hand and discarded cards, serialized by sort_id. --- src/game_objects/game_states.lua | 114 +++++++++++++++++++++++++------ src/utils.lua | 32 ++++++++- 2 files changed, 124 insertions(+), 22 deletions(-) diff --git a/src/game_objects/game_states.lua b/src/game_objects/game_states.lua index a6b1c56e0..35b834c36 100644 --- a/src/game_objects/game_states.lua +++ b/src/game_objects/game_states.lua @@ -61,6 +61,7 @@ end function SMODS.enter_state(new_state, enter_args, exit_args) local current_state = SMODS.STATE or G.STATE if current_state == new_state then return end + local next_held_state = SMODS.get_next_held_state() enter_args = enter_args or {} enter_args.old_state = current_state if SMODS.is_state_in_stack(new_state) then @@ -70,7 +71,7 @@ function SMODS.enter_state(new_state, enter_args, exit_args) end exit_args = exit_args or {} exit_args.new_state = exit_args.new_state or new_state - local exit_state_in_stack = SMODS.is_state_in_stack(current_state, true) + local exit_state_in_stack = not exit_args.from_hold and SMODS.is_state_in_stack(current_state, true) -- If exit_args.from_hold is already true, this is irrelevant if exit_state_in_stack then exit_args.from_hold = true end @@ -83,7 +84,9 @@ function SMODS.enter_state(new_state, enter_args, exit_args) G.STATE = new_state if SMODS.GameStates[new_state] then SMODS.STATE = new_state - SMODS.push_to_state_stack(new_state, enter_args) + if next_held_state ~= new_state or (exit_args.from_hold and not exit_state_in_stack) then + SMODS.push_to_state_stack(new_state, enter_args) + end SMODS.GameStates[new_state]:on_enter(enter_args) end end @@ -358,21 +361,26 @@ SMODS.GameState { })) end, on_exit = function (self, args) + G.CONTROLLER.locks.toggle_shop = true if args.from_hold then if G.shop and not G.shop.alignment.offset.py then G.shop.alignment.offset.py = G.shop.alignment.offset.y G.shop.alignment.offset.y = G.ROOM.T.y + 29 G.SHOP_SIGN.alignment.offset.y = -15 - delay(0.5) + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 0.5, + func = function () + G.CONTROLLER.locks.toggle_shop = nil + return true + end + })) end return end stop_use() - G.CONTROLLER.locks.toggle_shop = true if G.shop then - if not from_hold then - SMODS.calculate_context({ending_shop = true}) - end + SMODS.calculate_context({ending_shop = true}) G.E_MANAGER:add_event(Event({ trigger = 'immediate', func = function() @@ -381,17 +389,6 @@ SMODS.GameState { return true end })) - if from_hold then - G.E_MANAGER:add_event(Event({ - trigger = 'after', - delay = 0.5, - func = function () - G.CONTROLLER.locks.toggle_shop = nil - return true - end - })) - return - end G.E_MANAGER:add_event(Event({ trigger = 'after', delay = 0.5, @@ -514,7 +511,33 @@ SMODS.GameState { if args.force_refresh then self:on_exit({}) elseif args.from_hold then - -- Todo: Implement + G.GAME.facing_blind = true + local data = SMODS.state_stack[#SMODS.state_stack].data + ease_chips(data.chips) + G.GAME.blind:set_blind(G.P_BLINDS[data.blind_key], nil, true) + G.GAME.blind.chips = data.blind_chips + G.GAME.blind.chip_text = number_format(data.blind_chips) + ease_hands_played(G.GAME.current_round.hands_left - data.hands_left) + G.GAME.current_round.hands_played = data.hands_played + ease_discard(G.GAME.current_round.discards_left - data.discards_left) + G.GAME.current_round.discards_used = data.discards_used + for _, pcard_sort_id in ipairs(data.hand_cards) do + local pcard = SMODS.get_card_by_sort_id(pcard_sort_id) + if pcard and not pcard.removed then + draw_card(G.deck, G.hand, nil, nil, nil, pcard) + pcard.ability.forced_selection = data.forced_selection[pcard_sort_id] + end + end + for _, pcard_sort_id in ipairs(data.discarded_cards) do + local pcard = SMODS.get_card_by_sort_id(pcard_sort_id) + if pcard and not pcard.removed then + draw_card(G.deck, G.discard, nil, nil, nil, pcard) + pcard.ability.discarded = true + end + end + save_run() + G.STATE = G.STATES.SELECTING_HAND + G.CONTROLLER:recall_cardarea_focus('hand') return end G.E_MANAGER:add_event(Event({ @@ -555,7 +578,50 @@ SMODS.GameState { return end if args.from_hold then - -- Todo: implement holding SMODS.STATES.BLIND + if G.HUD_blind then + G.HUD_blind.alignment.offset.py = G.HUD_blind.alignment.offset.y + G.HUD_blind.alignment.offset.y = -10 + end + -- Todo : This should be made serializable, specifically hand_cards and discarded_cards + local data = { + blind_key = G.GAME.blind.config.blind.key, + hand_cards = {}, + forced_selection = {}, + discarded_cards = {}, + chips = G.GAME.chips, + blind_chips = G.GAME.blind.chips, + hands_left = G.GAME.current_round.hands_left, + hands_played = G.GAME.current_round.hands_played, + discards_left = G.GAME.current_round.discards_left, + discards_used = G.GAME.current_round.discards_used, + } + for _, pcard in ipairs(G.hand.cards) do + data.hand_cards[#data.hand_cards+1] = pcard.sort_id + if pcard.ability.forced_selection then + data.forced_selection[pcard.sort_id] = true + end + end + for _, pcard in ipairs(G.discard.cards) do + data.discarded_cards[#data.discarded_cards+1] = pcard.sort_id + end + SMODS.state_stack[#SMODS.state_stack].data = data + G.FUNCS.draw_from_hand_to_discard() + G.FUNCS.draw_from_discard_to_deck() + G.E_MANAGER:add_event(Event({ + trigger = "after", + blockable = false, + delay = 0.7, + func = function () + G.GAME.blind:disable() + if G.buttons then G.buttons:remove(); G.buttons = nil end + return true + end + })) + for k, v in ipairs(G.playing_cards) do + v.ability.discarded = nil + v.ability.forced_selection = nil + end + delay(0.4) return end G.E_MANAGER:add_event(Event({ @@ -631,6 +697,14 @@ SMODS.GameState { G.blind_select.alignment.offset.y = G.blind_select.alignment.offset.py G.blind_select.alignment.offset.py = nil end + G.E_MANAGER:add_event(Event({ + trigger = "after", + delay = 0.3, + func = function () + play_sound('cancel') + return true + end + })) return end G.E_MANAGER:add_event(Event({ diff --git a/src/utils.lua b/src/utils.lua index ed7c471eb..8e42630ab 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -2985,7 +2985,7 @@ end local game_start_run = Game.start_run function Game:start_run(args) - game_start_run(self, args) + local ret = game_start_run(self, args) G.SCORE_DISPLAY_QUEUE = nil G.E_MANAGER:add_event(Event({ trigger = 'immediate', @@ -2994,6 +2994,21 @@ function Game:start_run(args) return true end })) + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + if args.save_text and args.save_text.SMODS then + local state_data = args.save_text.SMODS.state_data + if state_data then + SMODS.STATE = state_data.STATE + SMODS.state_stack = state_data.state_stack or {} + SMODS.state_queue = state_data.state_queue or {} + end + end + return true + end + })) + return ret end G.FUNCS.SMODS_scoring_calculation_function = function(e) @@ -3800,10 +3815,15 @@ function save_run() smods_hook_save_run() if SMODS.last_hand and G.culled_table then G.culled_table.SMODS = { - last_hand = { + last_hand = { scoring_name = SMODS.last_hand.scoring_name, scoring_hand = {}, full_hand = {} + }, + state_data = { + STATE = SMODS.STATE, + state_stack = recursive_table_cull(SMODS.state_stack), + state_queue = recursive_table_cull(SMODS.state_queue), } } end @@ -4062,4 +4082,12 @@ end -- Simple unlock text function, created to give mod authors an option to hook rather than patch for their use cases. function SMODS.create_unlock_text(center) return localize('k_'..string.lower(center and center.set or 'unknown')) +end + +function SMODS.get_card_by_sort_id(sort_id) + for _, pcard in ipairs(G.playing_card) do + if pcard.sort_id == sort_id then + return pcard + end + end end \ No newline at end of file From 121bbb6ed4c928ce4fb109a07437496253fa3bf9 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Wed, 8 Apr 2026 00:10:49 +0200 Subject: [PATCH 26/39] Fixed / Improved saving and loading `BLIND` and `SHOP` states. +/*Title, I also added a new `SMODS.GameState` function; `on_load`. -> `SHOP` uses this to create and load `G.shop`. --- lovely/better_calc.toml | 2 +- lovely/game_state.toml | 21 ++++++- src/game_objects/game_states.lua | 99 +++++++++++++++++++++++++------- src/utils.lua | 55 ++++++++++++------ 4 files changed, 135 insertions(+), 42 deletions(-) diff --git a/lovely/better_calc.toml b/lovely/better_calc.toml index 80b6ba710..add5ebadb 100644 --- a/lovely/better_calc.toml +++ b/lovely/better_calc.toml @@ -2137,7 +2137,7 @@ table.sort(G.playing_cards, function (a, b) return a.playing_card > b.playing_ca ''' payload = ''' if saveTable then - if saveTable.SMODS then + if saveTable.SMODS and saveTable.SMODS.last_hand then SMODS.last_hand = {scoring_hand = {}, full_hand = {}, scoring_name = saveTable.SMODS.last_hand.scoring_name} for _, v in ipairs({'scoring_hand','full_hand'}) do for _, card in ipairs(G.playing_cards) do diff --git a/lovely/game_state.toml b/lovely/game_state.toml index b4568d372..0269b1711 100644 --- a/lovely/game_state.toml +++ b/lovely/game_state.toml @@ -21,7 +21,7 @@ if SMODS.GameStates[self.STATE] then end ''' -# Game:start_run() +# game.lua : Game:start_run() [[patches]] [patches.pattern] target = 'game.lua' @@ -33,7 +33,7 @@ self:prep_stage(G.STAGES.RUN, saveTable and saveTable.STATE or G.STATES.BLIND_SE payload = ''' self:prep_stage(G.STAGES.RUN, saveTable and saveTable.STATE or SMODS.STATES.BLIND_SELECT) ''' -# Game:prep_stage() +# game.lua : Game:prep_stage() [[patches]] [patches.pattern] target = 'game.lua' @@ -46,6 +46,23 @@ payload = ''' SMODS.enter_state(new_state or SELF.STATES.MENU) ''' +# game.lua : Game:update_selecting_hand(dt) / Game:update_hand_played(dt) / etc. : Avoid deleting the shop in case it is held in the SMODS.state_stack +[[patches]] +[patches.pattern] +target = 'game.lua' +match_indent = true +position = 'at' +pattern = '''if self.shop then self.shop:remove(); self.shop = nil end''' +payload = '''''' +[[patches]] # Repatch Game:delete_run() +[patches.pattern] +target = 'game.lua' +match_indent = true +position = 'after' +pattern = '''if self.deck_preview then self.deck_preview:remove(); self.deck_preview = nil end''' +payload = '''if self.shop then self.shop:remove(); self.shop = nil end''' + + # cardarea.lua : CardArea:draw() : update logic [[patches]] [patches.pattern] diff --git a/src/game_objects/game_states.lua b/src/game_objects/game_states.lua index 35b834c36..6f2c079ab 100644 --- a/src/game_objects/game_states.lua +++ b/src/game_objects/game_states.lua @@ -187,6 +187,7 @@ SMODS.GameState = SMODS.GameObject:extend{ end, on_enter = function (self, args) end, on_exit = function (self, args) end, + on_load = function (self) end, update = function (self, dt) end, ease_background_colour = nil, -- function exit_after_use_card = false, -- Used for consumable states like SMODS.STATES.REDEEM_VOUCHER @@ -208,14 +209,57 @@ SMODS.GameState { SMODS.GameState { key = SMODS.STATES.SHOP, + on_load = function () + G.shop = G.shop or UIBox{ + definition = G.UIDEF.shop(), + config = {align='tmi', offset = {x=0,y=G.ROOM.T.y+11},major = G.hand, bond = 'Weak'} + } + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = (function() + G.SHOP_SIGN.alignment.offset.y = -15 -- Counteracts the immediate event inside G.UIDEF.shop() + return true + end) + })) + + if G.load_shop_jokers then + G.shop_jokers:load(G.load_shop_jokers) + for k, v in ipairs(G.shop_jokers.cards) do + create_shop_card_ui(v) + if v.ability.consumeable then v:start_materialize(nil, true) end + for _kk, vvv in ipairs(G.GAME.tags) do + if vvv:apply_to_run({type = 'store_joker_modify', card = v}) then break end + end + end + G.load_shop_jokers = nil + end + + if G.load_shop_vouchers then + G.shop_vouchers:load(G.load_shop_vouchers) + for k, v in ipairs(G.shop_vouchers.cards) do + create_shop_card_ui(v) + v:start_materialize(nil, true) + end + G.load_shop_vouchers = nil + end + + if G.load_shop_booster then + G.shop_booster:load(G.load_shop_booster) + for k, v in ipairs(G.shop_booster.cards) do + create_shop_card_ui(v) + v:start_materialize(nil, true) + end + G.load_shop_booster = nil + end + end, on_enter = function (self, args) + G.CONTROLLER.locks.toggle_shop = nil if args.force_refresh then self:on_exit({}) elseif args.from_hold then - -- Todo : Store data to and restore data from SMODS.state_stack if G.shop then -- Extracted from G.FUNCS.use_card() - G.shop.alignment.offset.y = G.shop.alignment.offset.py + G.shop.alignment.offset.y = G.shop.alignment.offset.py or -5.3 G.shop.alignment.offset.py = nil G.SHOP_SIGN.alignment.offset.y = 0 @@ -238,6 +282,9 @@ SMODS.GameState { SMODS.calculate_context({starting_shop = true, from_hold = true}) G.CONTROLLER:snap_to({node = G.shop:get_UIE_by_ID('next_round_button')}) G.E_MANAGER:add_event(Event({ func = function() save_run(); return true end})) + else + args.force_refresh = true + self:on_enter(args) end return end @@ -367,15 +414,15 @@ SMODS.GameState { G.shop.alignment.offset.py = G.shop.alignment.offset.y G.shop.alignment.offset.y = G.ROOM.T.y + 29 G.SHOP_SIGN.alignment.offset.y = -15 - G.E_MANAGER:add_event(Event({ - trigger = 'after', - delay = 0.5, - func = function () - G.CONTROLLER.locks.toggle_shop = nil - return true - end - })) end + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 0.5, + func = function () + G.CONTROLLER.locks.toggle_shop = nil + return true + end + })) return end stop_use() @@ -402,6 +449,8 @@ SMODS.GameState { return true end })) + else + G.CONTROLLER.locks.toggle_shop = false end end, } @@ -415,7 +464,10 @@ SMODS.GameState { if G.round_eval then G.round_eval.alignment.offset.y = G.round_eval.alignment.offset.py G.round_eval.alignment.offset.py = nil - end + else + args.force_refresh = true + self:on_enter(args) + end return end G.E_MANAGER:add_event(Event({ @@ -521,15 +573,17 @@ SMODS.GameState { G.GAME.current_round.hands_played = data.hands_played ease_discard(G.GAME.current_round.discards_left - data.discards_left) G.GAME.current_round.discards_used = data.discards_used + local hand_cards = SMODS.get_cards_by_sort_ids(data.hand_cards) + local discarded_cards = SMODS.get_cards_by_sort_ids(data.discarded_cards) for _, pcard_sort_id in ipairs(data.hand_cards) do - local pcard = SMODS.get_card_by_sort_id(pcard_sort_id) + local pcard = hand_cards[pcard_sort_id] if pcard and not pcard.removed then draw_card(G.deck, G.hand, nil, nil, nil, pcard) pcard.ability.forced_selection = data.forced_selection[pcard_sort_id] end end for _, pcard_sort_id in ipairs(data.discarded_cards) do - local pcard = SMODS.get_card_by_sort_id(pcard_sort_id) + local pcard = discarded_cards[pcard_sort_id] if pcard and not pcard.removed then draw_card(G.deck, G.discard, nil, nil, nil, pcard) pcard.ability.discarded = true @@ -696,15 +750,18 @@ SMODS.GameState { if G.blind_select then G.blind_select.alignment.offset.y = G.blind_select.alignment.offset.py G.blind_select.alignment.offset.py = nil + G.E_MANAGER:add_event(Event({ + trigger = "after", + delay = 0.3, + func = function () + play_sound('cancel') + return true + end + })) + else + args.force_refresh = true + self:on_enter(args) end - G.E_MANAGER:add_event(Event({ - trigger = "after", - delay = 0.3, - func = function () - play_sound('cancel') - return true - end - })) return end G.E_MANAGER:add_event(Event({ diff --git a/src/utils.lua b/src/utils.lua index 8e42630ab..8c1e99d7d 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -2994,20 +2994,19 @@ function Game:start_run(args) return true end })) - G.E_MANAGER:add_event(Event({ - trigger = 'immediate', - func = function() - if args.save_text and args.save_text.SMODS then - local state_data = args.save_text.SMODS.state_data - if state_data then - SMODS.STATE = state_data.STATE - SMODS.state_stack = state_data.state_stack or {} - SMODS.state_queue = state_data.state_queue or {} + if args.savetext and args.savetext.SMODS then + local state_data = args.savetext.SMODS.state_data + if state_data then + SMODS.STATE = state_data.STATE + SMODS.state_stack = state_data.state_stack or {} + SMODS.state_queue = state_data.state_queue or {} + for _, state_table in ipairs(SMODS.state_stack) do + if SMODS.GameStates[state_table.state] then + SMODS.GameStates[state_table.state]:on_load() end end - return true end - })) + end return ret end @@ -3813,19 +3812,21 @@ function save_run() end end smods_hook_save_run() - if SMODS.last_hand and G.culled_table then + if G.culled_table then G.culled_table.SMODS = { - last_hand = { - scoring_name = SMODS.last_hand.scoring_name, - scoring_hand = {}, - full_hand = {} - }, state_data = { STATE = SMODS.STATE, state_stack = recursive_table_cull(SMODS.state_stack), state_queue = recursive_table_cull(SMODS.state_queue), } } + if SMODS.last_hand then + G.culled_table.SMODS.last_hand = { + scoring_name = SMODS.last_hand.scoring_name, + scoring_hand = {}, + full_hand = {} + } + end end end @@ -4085,9 +4086,27 @@ function SMODS.create_unlock_text(center) end function SMODS.get_card_by_sort_id(sort_id) - for _, pcard in ipairs(G.playing_card) do + for _, pcard in ipairs(G.playing_cards) do if pcard.sort_id == sort_id then return pcard end end +end + +function SMODS.get_cards_by_sort_ids(sort_ids) + local sort_ids_map = {} + if sort_ids[1] and type(sort_ids[1]) == "number" then + for _, sort_id in ipairs(sort_ids) do + sort_ids_map[sort_id] = true + end + else + sort_ids_map = sort_ids + end + local ret = {} + for _, pcard in ipairs(G.playing_cards) do + if sort_ids_map[pcard.sort_id] then + ret[pcard.sort_id] = pcard + end + end + return ret end \ No newline at end of file From afc238b5e0e1cbd8e852f3b089cf6042599305aa Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Wed, 8 Apr 2026 00:12:16 +0200 Subject: [PATCH 27/39] Removed Todo marker -Title --- src/game_objects/game_states.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/src/game_objects/game_states.lua b/src/game_objects/game_states.lua index 6f2c079ab..ac29e7f5b 100644 --- a/src/game_objects/game_states.lua +++ b/src/game_objects/game_states.lua @@ -636,7 +636,6 @@ SMODS.GameState { G.HUD_blind.alignment.offset.py = G.HUD_blind.alignment.offset.y G.HUD_blind.alignment.offset.y = -10 end - -- Todo : This should be made serializable, specifically hand_cards and discarded_cards local data = { blind_key = G.GAME.blind.config.blind.key, hand_cards = {}, From a8e2636f49511316879f0d59ba5e00570e4f2a5b Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Wed, 8 Apr 2026 00:56:39 +0200 Subject: [PATCH 28/39] Restored `end_round()` and `G.FUNCS.evaluate_round()` patches +/*Title - Removed overrides --- lovely/better_calc.toml | 4 +- lovely/blind.toml | 30 +++- lovely/compact_cashout.toml | 40 +++++ lovely/fixes.toml | 9 + lovely/game_state.toml | 52 ++++++ src/game_objects/game_states.lua | 48 +++--- src/overrides.lua | 271 ------------------------------- 7 files changed, 155 insertions(+), 299 deletions(-) diff --git a/lovely/better_calc.toml b/lovely/better_calc.toml index add5ebadb..09b641102 100644 --- a/lovely/better_calc.toml +++ b/lovely/better_calc.toml @@ -942,7 +942,7 @@ payload = ''' SMODS.saved = false G.GAME.saved_text = nil SMODS.last_hand = SMODS.last_hand or {scoring_hand = {}, full_hand = {}} -SMODS.calculate_context({end_of_round = true, game_over = game_over, beat_boss = G.GAME.blind.boss, scoring_hand = SMODS.last_hand.scoring_hand, scoring_name = SMODS.last_hand.scoring_name, full_hand = SMODS.last_hand.full_hand }) +SMODS.calculate_context({end_of_round = true, game_over = game_over, beat_boss = G.GAME.blind:is_type("Boss"), scoring_hand = SMODS.last_hand.scoring_hand, scoring_name = SMODS.last_hand.scoring_name, full_hand = SMODS.last_hand.full_hand }) if SMODS.saved then game_over = false end -- TARGET: main end_of_round evaluation ''' @@ -955,7 +955,7 @@ position = 'at' pattern = '''(?[\t ]*)for i=1, #G\.hand\.cards do\n\s+--Check for hand doubling\n(.*\n)*?\s+delay\(0\.3\)''' line_prepend = '$indent' payload = '''for _,v in ipairs(SMODS.get_card_areas('playing_cards', 'end_of_round')) do - SMODS.calculate_end_of_round_effects({ cardarea = v, end_of_round = true, beat_boss = G.GAME.blind.boss }) + SMODS.calculate_end_of_round_effects({ cardarea = v, end_of_round = true, beat_boss = G.GAME.blind:is_type("Boss") }) end ''' diff --git a/lovely/blind.toml b/lovely/blind.toml index 1f38a8414..a190e6053 100644 --- a/lovely/blind.toml +++ b/lovely/blind.toml @@ -594,10 +594,38 @@ position = "at" payload = ''' if G.GAME.modifiers.no_blind_reward and (G.GAME.blind and blind:is_types(G.GAME.modifiers.no_blind_reward)) then ''' +# functions/state_events.lua : end_round() +[[patches]] +[patches.pattern] +target = "functions/state_events.lua" +match_indent = true +pattern = '''if G.GAME.blind:get_type() == 'Boss' and not G.GAME.seeded and not G.GAME.challenge then''' +position = "at" +payload = ''' +if G.GAME.blind:is_type("Boss") and not G.GAME.seeded and not G.GAME.challenge then +''' +[[patches]] +[patches.pattern] +target = "functions/state_events.lua" +match_indent = true +pattern = '''if G.GAME.blind:get_type() == 'Boss' then''' +position = "at" +payload = ''' +if G.GAME.blind:is_type("Boss") then +''' +[[patches]] +[patches.pattern] +target = "functions/state_events.lua" +match_indent = true +pattern = '''if not G.GAME.won and G.GAME.round_resets.ante >= G.GAME.win_ante and G.GAME.blind_on_deck == 'Boss' then''' +position = "at" +payload = ''' +if G.GAME.round_resets.ante == G.GAME.win_ante and G.GAME.blind:is_type("Boss") then +''' # game.lua : Game:init_game_object() [[patches]] [patches.regex] target = "game.lua" pattern = '''if v.boss then bosses_used[k] = 0 end''' position = "at" -payload = '''if SMODS.get_blind_types(blind_obj).Boss then bosses_used[k] = 0 end''' \ No newline at end of file +payload = '''if SMODS.get_blind_types(blind_obj).Boss then bosses_used[k] = 0 end''' diff --git a/lovely/compact_cashout.toml b/lovely/compact_cashout.toml index 960607e0d..98676f316 100644 --- a/lovely/compact_cashout.toml +++ b/lovely/compact_cashout.toml @@ -22,4 +22,44 @@ payload = ''' end''' match_indent = true +# Reset rows amount + +[[patches]] +[patches.regex] +target = "functions/state_events.lua" +pattern = 'G\.FUNCS\.evaluate_round = function\(\)' +position = "after" +payload = ''' + + total_cashout_rows = 0''' + +# Add UI row with total rows hidden + +[[patches]] +[patches.pattern] +target = "functions/state_events.lua" +pattern = "add_round_eval_row({name = 'bottom', dollars = dollars})" +position = "before" +payload = ''' +if total_cashout_rows > 7 then + local total_hidden = total_cashout_rows - 7 + + G.E_MANAGER:add_event(Event({ + trigger = 'before',delay = 0.38, + func = function() + local hidden = {n=G.UIT.R, config={align = "cm"}, nodes={ + {n=G.UIT.O, config={object = DynaText({ + string = {localize{type = 'variable', key = 'cashout_hidden', vars = {total_hidden}}}, + colours = {G.C.WHITE}, shadow = true, float = false, + scale = 0.45, + font = G.LANGUAGES['en-us'].font, pop_in = 0 + })}} + }} + + G.round_eval:add_child(hidden, G.round_eval:get_UIE_by_ID('bonus_round_eval')) + return true + end + })) +end''' +match_indent = true diff --git a/lovely/fixes.toml b/lovely/fixes.toml index e2e21f067..90a6e2257 100644 --- a/lovely/fixes.toml +++ b/lovely/fixes.toml @@ -422,6 +422,15 @@ 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" +# end_round() +[[patches]] +[patches.pattern] +target = "functions/state_events.lua" +pattern = "local game_over = true" +position = "before" +payload = "G.GAME.blind.in_blind = false" +match_indent = true + # Allow winning game if winning ante is skipped [[patches]] [patches.pattern] diff --git a/lovely/game_state.toml b/lovely/game_state.toml index 0269b1711..a9c4a0f4a 100644 --- a/lovely/game_state.toml +++ b/lovely/game_state.toml @@ -177,4 +177,56 @@ if SMODS.GameStates[G.STATE] and SMODS.GameStates[G.STATE].exit_after_end_consum elseif not SMODS.GameStates[G.STATE] then G.STATE = G.GAME.PACK_INTERRUPT end +''' + +# functions/state_events.lua : G.FUNCS.evaluate_round() : remove Blind:defeat() call -> moved to SMODS.STATES.BLIND's on_exit() +[[patches]] +[patches.pattern] +target = 'functions/state_events.lua' +match_indent = true +position = 'at' +pattern = ''' +G.E_MANAGER:add_event(Event({ + trigger = 'before', + delay = 1.3*math.min(G.GAME.blind.dollars+2, 7)/2*0.15 + 0.5, + func = function() + G.GAME.blind:defeat() + return true + end + })) +delay(0.2) +''' +payload = ''' +-- Blind:defeat() call moved to SMODS.STATES.BLIND's on_exit() +''' + +# functions/state_events.lua : end_round() : update logic +[[patches]] +[patches.pattern] +target = 'functions/state_events.lua' +match_indent = true +position = 'at' +pattern = '''G.STATE = G.STATES.ROUND_EVAL''' +payload = ''' +if #SMODS.state_queue == 0 then + SMODS.queue_state(SMODS.STATES.ROUND_EVAL) +end +SMODS.advance_state_queue() +''' +[[patches]] +[patches.pattern] +target = 'functions/state_events.lua' +match_indent = true +position = 'at' +pattern = ''' +if G.STATE == G.STATES.ROUND_EVAL then + win_game() + G.GAME.won = true + return true +end +''' +payload = ''' +win_game() +G.GAME.won = true +return true ''' \ No newline at end of file diff --git a/src/game_objects/game_states.lua b/src/game_objects/game_states.lua index ac29e7f5b..4547556dd 100644 --- a/src/game_objects/game_states.lua +++ b/src/game_objects/game_states.lua @@ -627,37 +627,35 @@ SMODS.GameState { end, on_exit = function (self, args) G.GAME.facing_blind = nil - if args.no_defeat then -- Example: SMODS.enter_state(SMODS.STATES.SHOP, nil, {no_defeat = true}) -> This opens the shop without defeating the blind or holding SMODS.STATES.BLIND - -- Todo: implement this - return - end - if args.from_hold then + if args.from_hold or args.no_defeat then if G.HUD_blind then G.HUD_blind.alignment.offset.py = G.HUD_blind.alignment.offset.y G.HUD_blind.alignment.offset.y = -10 end - local data = { - blind_key = G.GAME.blind.config.blind.key, - hand_cards = {}, - forced_selection = {}, - discarded_cards = {}, - chips = G.GAME.chips, - blind_chips = G.GAME.blind.chips, - hands_left = G.GAME.current_round.hands_left, - hands_played = G.GAME.current_round.hands_played, - discards_left = G.GAME.current_round.discards_left, - discards_used = G.GAME.current_round.discards_used, - } - for _, pcard in ipairs(G.hand.cards) do - data.hand_cards[#data.hand_cards+1] = pcard.sort_id - if pcard.ability.forced_selection then - data.forced_selection[pcard.sort_id] = true + if args.from_hold then + local data = { + blind_key = G.GAME.blind.config.blind.key, + hand_cards = {}, + forced_selection = {}, + discarded_cards = {}, + chips = G.GAME.chips, + blind_chips = G.GAME.blind.chips, + hands_left = G.GAME.current_round.hands_left, + hands_played = G.GAME.current_round.hands_played, + discards_left = G.GAME.current_round.discards_left, + discards_used = G.GAME.current_round.discards_used, + } + for _, pcard in ipairs(G.hand.cards) do + data.hand_cards[#data.hand_cards+1] = pcard.sort_id + if pcard.ability.forced_selection then + data.forced_selection[pcard.sort_id] = true + end end + for _, pcard in ipairs(G.discard.cards) do + data.discarded_cards[#data.discarded_cards+1] = pcard.sort_id + end + SMODS.state_stack[#SMODS.state_stack].data = data end - for _, pcard in ipairs(G.discard.cards) do - data.discarded_cards[#data.discarded_cards+1] = pcard.sort_id - end - SMODS.state_stack[#SMODS.state_stack].data = data G.FUNCS.draw_from_hand_to_discard() G.FUNCS.draw_from_discard_to_deck() G.E_MANAGER:add_event(Event({ diff --git a/src/overrides.lua b/src/overrides.lua index b3b8b03ed..6a3e7f8fe 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -2765,275 +2765,4 @@ function ease_background_colour_blind(state, blind_override) return SMODS.GameStates[state]:ease_background_colour(blind_override) end return ease_bg_col_bl_ref(state, blind_override) -end - -function end_round() - G.E_MANAGER:add_event(Event({ - trigger = 'after', - delay = 0.2, - func = function() - G.GAME.blind.in_blind = false - local game_over = true - local game_won = false - G.RESET_BLIND_STATES = true - G.RESET_JIGGLES = true - if G.GAME.chips - G.GAME.blind.chips >= 0 then - game_over = false - end - -- context.end_of_round calculations - SMODS.saved = false - G.GAME.saved_text = nil - SMODS.calculate_context({end_of_round = true, game_over = game_over, beat_boss = G.GAME.blind:is_type("Boss") }) - if SMODS.saved then game_over = false end - -- TARGET: main end_of_round evaluation - if G.GAME.round_resets.ante == G.GAME.win_ante and G.GAME.blind:is_type("Boss") then - game_won = true - G.GAME.won = true - end - if game_over then - G.STATE = G.STATES.GAME_OVER - if not G.GAME.won and not G.GAME.seeded and not G.GAME.challenge then - G.PROFILES[G.SETTINGS.profile].high_scores.current_streak.amt = 0 - end - G:save_settings() - G.FILE_HANDLER.force = true - G.STATE_COMPLETE = false - else - G.GAME.unused_discards = (G.GAME.unused_discards or 0) + G.GAME.current_round.discards_left - if G.GAME.blind and G.GAME.blind.config.blind then - discover_card(G.GAME.blind.config.blind) - end - - if G.GAME.blind:is_type("Boss") then - local _handname, _played, _order = 'High Card', -1, 100 - for k, v in pairs(G.GAME.hands) do - if v.played > _played or (v.played == _played and _order > v.order) then - _played = v.played - _handname = k - end - end - G.GAME.current_round.most_played_poker_hand = _handname - end - - if G.GAME.blind:is_type("Boss") and not G.GAME.seeded and not G.GAME.challenge then - G.GAME.current_boss_streak = G.GAME.current_boss_streak + 1 - check_and_set_high_score('boss_streak', G.GAME.current_boss_streak) - end - - if G.GAME.current_round.hands_played == 1 then - inc_career_stat('c_single_hand_round_streak', 1) - else - if not G.GAME.seeded and not G.GAME.challenge then - G.PROFILES[G.SETTINGS.profile].career_stats.c_single_hand_round_streak = 0 - G:save_settings() - end - end - - check_for_unlock({type = 'round_win'}) - set_joker_usage() - if game_won and not G.GAME.win_notified then - G.GAME.win_notified = true - G.E_MANAGER:add_event(Event({ - trigger = 'immediate', - blocking = false, - blockable = false, - func = (function() - win_game() - G.GAME.won = true - return true - end) - })) - end - for _,v in ipairs(SMODS.get_card_areas('playing_cards', 'end_of_round')) do - SMODS.calculate_end_of_round_effects({ cardarea = v, end_of_round = true, beat_boss = G.GAME.blind.boss }) - end - - G.FUNCS.draw_from_hand_to_discard() - if G.GAME.blind:is_type("Boss") then - G.GAME.voucher_restock = nil - for k, v in pairs(G.GAME.hands) do - v.played_this_ante = 0 - end - if G.GAME.modifiers.set_eternal_ante and (G.GAME.round_resets.ante == G.GAME.modifiers.set_eternal_ante) then - for k, v in ipairs(G.jokers.cards) do - v:set_eternal(true) - end - end - if G.GAME.modifiers.set_joker_slots_ante and (G.GAME.round_resets.ante == G.GAME.modifiers.set_joker_slots_ante) then - G.jokers.config.card_limit = 0 - end - delay(0.4); ease_ante(1); delay(0.4); check_for_unlock({type = 'ante_up', ante = G.GAME.round_resets.ante + 1}) - end - G.FUNCS.draw_from_discard_to_deck() - G.E_MANAGER:add_event(Event({ - trigger = 'after', - delay = 0.3, - func = function() - if #SMODS.state_queue == 0 then - SMODS.queue_state(SMODS.STATES.ROUND_EVAL) - end - SMODS.advance_state_queue() - G.STATE_COMPLETE = false - - if G.GAME.round_resets.blind == G.P_BLINDS.bl_small then - G.GAME.round_resets.blind_states.Small = 'Defeated' - elseif G.GAME.round_resets.blind == G.P_BLINDS.bl_big then - G.GAME.round_resets.blind_states.Big = 'Defeated' - else - G.GAME.current_round.voucher = SMODS.get_next_vouchers() - G.GAME.round_resets.blind_states.Boss = 'Defeated' - for k, v in ipairs(G.playing_cards) do - v.ability.played_this_ante = nil - end - end - - if G.GAME.round_resets.temp_handsize then G.hand:change_size(-G.GAME.round_resets.temp_handsize); G.GAME.round_resets.temp_handsize = nil end - if G.GAME.round_resets.temp_reroll_cost then G.GAME.round_resets.temp_reroll_cost = nil; calculate_reroll_cost(true) end - - reset_idol_card() - reset_mail_rank() - reset_ancient_card() - reset_castle_card() - for _, mod in ipairs(SMODS.mod_list) do - if mod.reset_game_globals and type(mod.reset_game_globals) == 'function' then - mod.reset_game_globals(false) - end - end - for k, v in ipairs(G.playing_cards) do - v.ability.discarded = nil - v.ability.forced_selection = nil - end - return true - end - })) - end - return true - end - })) -end - -G.FUNCS.evaluate_round = function() - total_cashout_rows = 0 - local pitch = 0.95 - local dollars = 0 - - if G.GAME.chips - G.GAME.blind.chips >= 0 then - add_round_eval_row({dollars = G.GAME.blind.dollars, name='blind1', pitch = pitch}) - pitch = pitch + 0.06 - dollars = dollars + G.GAME.blind.dollars - else - add_round_eval_row({dollars = 0, name='blind1', pitch = pitch, saved = true}) - pitch = pitch + 0.06 - end - - G.E_MANAGER:add_event(Event({ - func = function() - ease_background_colour_blind(G.STATES.ROUND_EVAL, '') - return true - end - })) - SMODS.calculate_context{round_eval = true} - G.GAME.selected_back:trigger_effect({context = 'eval'}) - - if G.GAME.current_round.hands_left > 0 and not G.GAME.modifiers.no_extra_hand_money then - add_round_eval_row({dollars = G.GAME.current_round.hands_left*(G.GAME.modifiers.money_per_hand or 1), disp = G.GAME.current_round.hands_left, bonus = true, name='hands', pitch = pitch}) - pitch = pitch + 0.06 - dollars = dollars + G.GAME.current_round.hands_left*(G.GAME.modifiers.money_per_hand or 1) - end - if G.GAME.current_round.discards_left > 0 and G.GAME.modifiers.money_per_discard then - add_round_eval_row({dollars = G.GAME.current_round.discards_left*(G.GAME.modifiers.money_per_discard), disp = G.GAME.current_round.discards_left, bonus = true, name='discards', pitch = pitch}) - pitch = pitch + 0.06 - dollars = dollars + G.GAME.current_round.discards_left*(G.GAME.modifiers.money_per_discard) - end - local i = 0 - for _, area in ipairs(SMODS.get_card_areas('jokers')) do - for _, _card in ipairs(area.cards) do - local ret, ret_opts = _card:calculate_dollar_bonus() - ret_opts = ret_opts or {} - - -- TARGET: calc_dollar_bonus per card - if ret then - if not ret_opts.no_eval_row then - i = i+1 - add_round_eval_row({dollars = ret, bonus = true, name='joker'..i, pitch = pitch, card = _card, loc_opts = ret_opts}) - pitch = pitch + 0.06 - end - dollars = dollars + ret - end - end - end - i = 0 - for _, target in ipairs(SMODS.get_card_areas('individual', 'calc_dollar_bonus')) do - if type(target.object.calc_dollar_bonus) == 'function' then - local ret, ret_opts = target.object:calc_dollar_bonus() - ret_opts = ret_opts or {} - local name - - -- TARGET: calc_dollar_bonus per individual object - if ret then - if not ret_opts.no_eval_row then - if ret_opts.text then - name = text - elseif (ret_opts.set or target.set) and (ret_opts.key or target.key) then - if ret_opts.set == "Challenge" or (not ret_opts.set and target.set == "Challenge") then - name = localize(ret_opts.key or target.key, 'challenge_names') - elseif (ret_opts.set == "Mod" or (not ret_opts.set and target.set == "Mod")) and not (G.localization.descriptions.Mod or {})[ret_opts.key or target.key] then - name = (SMODS.Mods[ret_opts.key or target.key] or {}).name - else - name = localize{type = 'name_text', set = ret_opts.set or target.set, key = ret_opts.key or target.key} - end - end - i = i+1 - add_round_eval_row({dollars = ret, bonus = true, name='custom_individual'..i, pitch = pitch, text_colour = ret_opts.text_colour or G.C.FILTER, text = name or 'ERROR', text_scale = ret_opts.scale or 0.6}) - pitch = pitch + 0.06 - end - dollars = dollars + ret - end - end - end - for i = 1, #G.GAME.tags do - local ret = G.GAME.tags[i]:apply_to_run({type = 'eval'}) - if ret then - add_round_eval_row({dollars = ret.dollars, bonus = true, name='tag'..i, pitch = pitch, condition = ret.condition, pos = ret.pos, tag = ret.tag}) - pitch = pitch + 0.06 - dollars = dollars + ret.dollars - end - end - if G.GAME.dollars >= 5 and not G.GAME.modifiers.no_interest then - add_round_eval_row({bonus = true, name='interest', pitch = pitch, dollars = G.GAME.interest_amount*math.min(math.floor(G.GAME.dollars/5), G.GAME.interest_cap/5)}) - pitch = pitch + 0.06 - if (not G.GAME.seeded and not G.GAME.challenge) or SMODS.config.seeded_unlocks then - if G.GAME.interest_amount*math.min(math.floor(G.GAME.dollars/5), G.GAME.interest_cap/5) == G.GAME.interest_amount*G.GAME.interest_cap/5 then - G.PROFILES[G.SETTINGS.profile].career_stats.c_round_interest_cap_streak = G.PROFILES[G.SETTINGS.profile].career_stats.c_round_interest_cap_streak + 1 - else - G.PROFILES[G.SETTINGS.profile].career_stats.c_round_interest_cap_streak = 0 - end - end - check_for_unlock({type = 'interest_streak'}) - dollars = dollars + G.GAME.interest_amount*math.min(math.floor(G.GAME.dollars/5), G.GAME.interest_cap/5) - end - - pitch = pitch + 0.06 - - if total_cashout_rows > 7 then - local total_hidden = total_cashout_rows - 7 - - G.E_MANAGER:add_event(Event({ - trigger = 'before',delay = 0.38, - func = function() - local hidden = {n=G.UIT.R, config={align = "cm"}, nodes={ - {n=G.UIT.O, config={object = DynaText({ - string = {localize{type = 'variable', key = 'cashout_hidden', vars = {total_hidden}}}, - colours = {G.C.WHITE}, shadow = true, float = false, - scale = 0.45, - font = G.LANGUAGES['en-us'].font, pop_in = 0 - })}} - }} - - G.round_eval:add_child(hidden, G.round_eval:get_UIE_by_ID('bonus_round_eval')) - return true - end - })) - end - add_round_eval_row({name = 'bottom', dollars = dollars}) end \ No newline at end of file From fae6f610eda632b117b8d4a601d51553a52fd7ff Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Wed, 8 Apr 2026 00:58:41 +0200 Subject: [PATCH 29/39] Missing patch +/*Title --- lovely/mod.toml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lovely/mod.toml b/lovely/mod.toml index cd4d5deda..8feb0d80f 100644 --- a/lovely/mod.toml +++ b/lovely/mod.toml @@ -5,6 +5,21 @@ priority = -5 ### Per-mod functions +# end_round() +[[patches]] +[patches.regex] +target = 'functions/state_events.lua' +pattern = '''(?[\t ]*)reset_castle_card\(\)''' +line_prepend = '$indent' +position = 'after' +payload = ''' + +for _, mod in ipairs(SMODS.mod_list) do + if mod.reset_game_globals and type(mod.reset_game_globals) == 'function' then + mod.reset_game_globals(false) + end +end''' + # Game:start_run() [[patches]] [patches.regex] From 5a35ba3b6d7341044c6bc87726936625f74f3a1b Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Wed, 8 Apr 2026 01:42:22 +0200 Subject: [PATCH 30/39] Updated `SMODS.get_new_blind()` and according `weight` function *Title, changes are yet untested! ! The showdown check isn't needed anymore because get_new_boss() already changes it's requested blind type according to the showdown ante. --- src/game_object.lua | 5 +++-- src/utils/weights.lua | 31 ++++++++++++++++--------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/game_object.lua b/src/game_object.lua index 9f560e874..e7f6959c8 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -1866,7 +1866,7 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. function SMODS.get_new_blind(blind_types) -- Use SMODS object weight system when enabled - if SMODS.optional_features.object_weights then return SMODS.poll_object({type = 'Blind'}) end + if SMODS.optional_features.object_weights then return SMODS.poll_object({type = 'Blind', blind_types = blind_types}) end if not blind_types or (blind_types.Boss or blind_types.Showdown) then G.GAME.perscribed_bosses = G.GAME.perscribed_bosses or {} if G.GAME.perscribed_bosses and G.GAME.perscribed_bosses[G.GAME.round_resets.ante] then @@ -1883,9 +1883,10 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. local res = SMODS.add_to_pool(v) if res then local b_types = SMODS.get_blind_types(v) + local blind_config = v.boss or (table_length(b_types) == 1 and next(b_types)) or {} for b_type, _ in pairs(b_types) do if blind_types[b_type] then - if v.in_pool or not (v.boss and v.boss.min and v.boss.min > math.max(1, G.GAME.round_resets.ante)) then + if v.in_pool or not (blind_config and (blind_config.min and blind_config.min > math.max(1, G.GAME.round_resets.ante) or (blind_config.max or G.GAME.round_resets.ante) >= G.GAME.round_resets.ante)) then eligible_bosses[k] = true break end diff --git a/src/utils/weights.lua b/src/utils/weights.lua index b40f4cdda..b0a4a912a 100644 --- a/src/utils/weights.lua +++ b/src/utils/weights.lua @@ -145,25 +145,26 @@ function SMODS.get_poll_key(type, infill) return t.str .. (t.block_infill and "" or infill or "") .. (t.ante and G.GAME.round_resets.ante or "") end -function SMODS.create_blind_pool(blind_type, skip_cull) - assert(type(blind_type) == 'string', "SMODS.create_blind_pool called with a non-string type argument."..SMODS.log_crash_info(debug.getinfo(2))) +function SMODS.create_blind_pool(_blind_type, skip_cull) + assert(type(_blind_type) == 'string' or type(_blind_type) == 'table', "SMODS.create_blind_pool called with an invalid argument."..SMODS.log_crash_info(debug.getinfo(2))) + local blind_types + if type(_blind_type) == "string" then + blind_types = {[_blind_type] = true} + else + blind_types = _blind_type + end local eligible_bosses = {} + _blind_type = (table_length(blind_types) == 1 and next(blind_types)) or "" -- If a string was passed, remains unchanged. for k, v in pairs(G.P_BLINDS) do local res, options = SMODS.add_to_pool(v) options = options or {} - if not v[blind_type] then - elseif options.ignore_showdown_check then + local blind_config = v.boss or v[_blind_type] or {} + if v:is_types(blind_types) and ( + v.in_pool or blind_config and ((blind_config.min or G.GAME.round_resets.ante) <= math.max(1, G.GAME.round_resets.ante) and + (blind_config.max or G.GAME.round_resets.ante) >= G.GAME.round_resets.ante) + ) + then eligible_bosses[k] = res and true or nil - elseif blind_type == 'boss' then - if - ((SMODS.is_showdown_ante()) == (v.boss.showdown or false)) and ((v[blind_type].min or G.GAME.round_resets.ante) <= math.max(1, G.GAME.round_resets.ante)) and ((v[blind_type].max or G.GAME.round_resets.ante) >= G.GAME.round_resets.ante) - then - eligible_bosses[k] = res and true or nil - end - else - if (v[blind_type].min or G.GAME.round_resets.ante) <= math.max(1, G.GAME.round_resets.ante) and (v[blind_type].max or G.GAME.round_resets.ante) >= G.GAME.round_resets.ante then - eligible_bosses[k] = res and true or nil - end end end for k, v in pairs(G.GAME.banned_keys) do @@ -286,7 +287,7 @@ function SMODS.create_poll_pool(labels, args) local temp_pool = {} local join_func = (args.attributes and not args.union) and SMODS.intersect_lists or join_lists for i=1, #(args.rarities or {true}) do - local _p = label == 'Blind' and SMODS.create_blind_pool(args.blind_type or 'boss') or SMODS.Attributes[label] and SMODS.get_attribute_pool(label) or get_current_pool(label, args.rarities and args.rarities[i], nil, args.append) + local _p = label == 'Blind' and SMODS.create_blind_pool(args.blind_types or {Boss = true}) or SMODS.Attributes[label] and SMODS.get_attribute_pool(label) or get_current_pool(label, args.rarities and args.rarities[i], nil, args.append) if SMODS.Attributes[label] then _p = SMODS.cull_pool(_p, args) end From 35236c434617fc0152222e09856b1c09f5d1ebcb Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Wed, 8 Apr 2026 01:43:44 +0200 Subject: [PATCH 31/39] Minor refactor *Title, updated `get_new_boss()` to use `SMODS.is_showdown_ante()`. --- src/game_object.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game_object.lua b/src/game_object.lua index e7f6959c8..ebc033f33 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -1859,7 +1859,7 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. function get_new_boss() -- sendWarnMessage("get_new_boss() is deprecated; Call SMODS.get_new_blind() instead.", "utils") - local b_types = G.GAME.round_resets.ante % G.GAME.win_ante == 0 and {Showdown = true} or {Boss = true} + local b_types = SMODS.is_showdown_ante() and {Showdown = true} or {Boss = true} local boss = SMODS.get_new_blind(b_types) return boss end From 4b325c7eefc4877a4374a6f0bcf8201673e6a2b6 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Wed, 8 Apr 2026 02:13:08 +0200 Subject: [PATCH 32/39] Fixed Blind related crash + added `args.leave_sign` to `SHOP` state's `on_exit({from_hold=true})` */+Title --- lovely/booster.toml | 2 +- src/game_object.lua | 2 +- src/game_objects/game_states.lua | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lovely/booster.toml b/lovely/booster.toml index c3ffb3b3f..12586f7ca 100644 --- a/lovely/booster.toml +++ b/lovely/booster.toml @@ -341,7 +341,7 @@ target = 'card.lua' match_indent = true position = 'after' pattern = '''if self.shop_voucher then G.GAME.current_round.voucher = nil end''' -payload = '''SMODS.enter_state(SMODS.STATES.REDEEM_VOUCHER, nil, {from_hold = true})''' +payload = '''SMODS.enter_state(SMODS.STATES.REDEEM_VOUCHER, nil, {from_hold = true, leave_sign = true})''' # G.FUNCS.use_card() # Consumables in areas other than the consumable don't count diff --git a/src/game_object.lua b/src/game_object.lua index ebc033f33..4b90d31b4 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -1886,7 +1886,7 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. local blind_config = v.boss or (table_length(b_types) == 1 and next(b_types)) or {} for b_type, _ in pairs(b_types) do if blind_types[b_type] then - if v.in_pool or not (blind_config and (blind_config.min and blind_config.min > math.max(1, G.GAME.round_resets.ante) or (blind_config.max or G.GAME.round_resets.ante) >= G.GAME.round_resets.ante)) then + if v.in_pool or not (blind_config and (blind_config.min and blind_config.min > math.max(1, G.GAME.round_resets.ante) or blind_config.max and blind_config.max <= G.GAME.round_resets.ante)) then eligible_bosses[k] = true break end diff --git a/src/game_objects/game_states.lua b/src/game_objects/game_states.lua index 4547556dd..c1ba32a3e 100644 --- a/src/game_objects/game_states.lua +++ b/src/game_objects/game_states.lua @@ -413,7 +413,9 @@ SMODS.GameState { if G.shop and not G.shop.alignment.offset.py then G.shop.alignment.offset.py = G.shop.alignment.offset.y G.shop.alignment.offset.y = G.ROOM.T.y + 29 - G.SHOP_SIGN.alignment.offset.y = -15 + if not args.leave_sign then + G.SHOP_SIGN.alignment.offset.y = -15 + end end G.E_MANAGER:add_event(Event({ trigger = 'after', From 91a899d1d8f77bf8d222204b2d3bad0ea5aaa941 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Wed, 8 Apr 2026 11:34:38 +0200 Subject: [PATCH 33/39] Moved `blind_types` and related over to new PR */-Title --- src/game_object.lua | 122 ------------------------------------------ src/utils/weights.lua | 31 ++++++----- 2 files changed, 15 insertions(+), 138 deletions(-) diff --git a/src/game_object.lua b/src/game_object.lua index 4b90d31b4..6352cc704 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -1808,7 +1808,6 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. config = {}, dollars = 5, mult = 2, - blind_types = nil, -- Map of types, used by SMODS.get_new_blind() atlas = 'blind_chips', discovered = false, pos = { x = 0, y = 0 }, @@ -1828,130 +1827,9 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. end G.P_BLINDS[self.key] = self if self.modifies_draw then SMODS.Blinds.modifies_draw[self.key] = true end - end, - get_types = function(self) - return self.blind_types end } - function SMODS.get_blind_types(blind_obj) - local ret - if type(blind_obj.get_types) == "function" then - ret = blind_obj:get_types() - end - if not ret then - if blind_obj.boss then - if not blind_obj.boss.showdown then - return {Boss = true} - else - return {Showdown = true} - end - else - if blind_obj.name == "Small Blind" then - return {Small = true} - else - return {Big = true} - end - end - end - return ret - end - - function get_new_boss() - -- sendWarnMessage("get_new_boss() is deprecated; Call SMODS.get_new_blind() instead.", "utils") - local b_types = SMODS.is_showdown_ante() and {Showdown = true} or {Boss = true} - local boss = SMODS.get_new_blind(b_types) - return boss - end - - function SMODS.get_new_blind(blind_types) - -- Use SMODS object weight system when enabled - if SMODS.optional_features.object_weights then return SMODS.poll_object({type = 'Blind', blind_types = blind_types}) end - if not blind_types or (blind_types.Boss or blind_types.Showdown) then - G.GAME.perscribed_bosses = G.GAME.perscribed_bosses or {} - if G.GAME.perscribed_bosses and G.GAME.perscribed_bosses[G.GAME.round_resets.ante] then - local ret_boss = G.GAME.perscribed_bosses[G.GAME.round_resets.ante] - G.GAME.perscribed_bosses[G.GAME.round_resets.ante] = nil - G.GAME.bosses_used[ret_boss] = G.GAME.bosses_used[ret_boss] + 1 - return ret_boss - end - if G.FORCE_BOSS then return G.FORCE_BOSS end - end - - local eligible_bosses = {} - for k, v in pairs(G.P_BLINDS) do - local res = SMODS.add_to_pool(v) - if res then - local b_types = SMODS.get_blind_types(v) - local blind_config = v.boss or (table_length(b_types) == 1 and next(b_types)) or {} - for b_type, _ in pairs(b_types) do - if blind_types[b_type] then - if v.in_pool or not (blind_config and (blind_config.min and blind_config.min > math.max(1, G.GAME.round_resets.ante) or blind_config.max and blind_config.max <= G.GAME.round_resets.ante)) then - eligible_bosses[k] = true - break - end - end - end - end - end - for k, v in pairs(G.GAME.banned_keys) do - if eligible_bosses[k] then eligible_bosses[k] = nil end - end - - local min_use = 100 - for k, v in pairs(G.GAME.bosses_used) do - if eligible_bosses[k] then - eligible_bosses[k] = v - if eligible_bosses[k] <= min_use then - min_use = eligible_bosses[k] - end - end - end - for k, v in pairs(eligible_bosses) do - if eligible_bosses[k] then - if eligible_bosses[k] > min_use then - eligible_bosses[k] = nil - end - end - end - local _, boss = pseudorandom_element(eligible_bosses, pseudoseed('boss')) - G.GAME.bosses_used[boss] = G.GAME.bosses_used[boss] + 1 - - return boss - end - - function Blind:get_type() - return SMODS.get_blind_types(self.config.blind) - end - - function Blind:is_type(b_type) - return self:is_types({[b_type] = true}, false) - end - - function Blind:is_types(b_types_map, all) - for k, v in pairs(self:get_type()) do - if v and b_types_map[k] or (k == "Showdown" and b_types_map.Boss) then - if not all then - return true - end - elseif all then - return false - end - end - return all - end - - SMODS.Joker:take_ownership("j_matador", { - check_for_unlock = function (self, args) - return G.GAME.current_round.hands_played == 1 and G.GAME.current_round.discards_left == G.GAME.round_resets.discards and G.GAME.blind:is_type("Boss") - end - }) - - SMODS.Joker:take_ownership("j_hanging_chad", { - check_for_unlock = function (self, args) - return G.GAME.last_hand_played == self.unlock_condition.extra and G.GAME.blind:is_type("Boss") - end - }) SMODS.Blind:take_ownership('eye', { set_blind = function(self, reset, silent) if not reset then diff --git a/src/utils/weights.lua b/src/utils/weights.lua index b0a4a912a..b40f4cdda 100644 --- a/src/utils/weights.lua +++ b/src/utils/weights.lua @@ -145,26 +145,25 @@ function SMODS.get_poll_key(type, infill) return t.str .. (t.block_infill and "" or infill or "") .. (t.ante and G.GAME.round_resets.ante or "") end -function SMODS.create_blind_pool(_blind_type, skip_cull) - assert(type(_blind_type) == 'string' or type(_blind_type) == 'table', "SMODS.create_blind_pool called with an invalid argument."..SMODS.log_crash_info(debug.getinfo(2))) - local blind_types - if type(_blind_type) == "string" then - blind_types = {[_blind_type] = true} - else - blind_types = _blind_type - end +function SMODS.create_blind_pool(blind_type, skip_cull) + assert(type(blind_type) == 'string', "SMODS.create_blind_pool called with a non-string type argument."..SMODS.log_crash_info(debug.getinfo(2))) local eligible_bosses = {} - _blind_type = (table_length(blind_types) == 1 and next(blind_types)) or "" -- If a string was passed, remains unchanged. for k, v in pairs(G.P_BLINDS) do local res, options = SMODS.add_to_pool(v) options = options or {} - local blind_config = v.boss or v[_blind_type] or {} - if v:is_types(blind_types) and ( - v.in_pool or blind_config and ((blind_config.min or G.GAME.round_resets.ante) <= math.max(1, G.GAME.round_resets.ante) and - (blind_config.max or G.GAME.round_resets.ante) >= G.GAME.round_resets.ante) - ) - then + if not v[blind_type] then + elseif options.ignore_showdown_check then eligible_bosses[k] = res and true or nil + elseif blind_type == 'boss' then + if + ((SMODS.is_showdown_ante()) == (v.boss.showdown or false)) and ((v[blind_type].min or G.GAME.round_resets.ante) <= math.max(1, G.GAME.round_resets.ante)) and ((v[blind_type].max or G.GAME.round_resets.ante) >= G.GAME.round_resets.ante) + then + eligible_bosses[k] = res and true or nil + end + else + if (v[blind_type].min or G.GAME.round_resets.ante) <= math.max(1, G.GAME.round_resets.ante) and (v[blind_type].max or G.GAME.round_resets.ante) >= G.GAME.round_resets.ante then + eligible_bosses[k] = res and true or nil + end end end for k, v in pairs(G.GAME.banned_keys) do @@ -287,7 +286,7 @@ function SMODS.create_poll_pool(labels, args) local temp_pool = {} local join_func = (args.attributes and not args.union) and SMODS.intersect_lists or join_lists for i=1, #(args.rarities or {true}) do - local _p = label == 'Blind' and SMODS.create_blind_pool(args.blind_types or {Boss = true}) or SMODS.Attributes[label] and SMODS.get_attribute_pool(label) or get_current_pool(label, args.rarities and args.rarities[i], nil, args.append) + local _p = label == 'Blind' and SMODS.create_blind_pool(args.blind_type or 'boss') or SMODS.Attributes[label] and SMODS.get_attribute_pool(label) or get_current_pool(label, args.rarities and args.rarities[i], nil, args.append) if SMODS.Attributes[label] then _p = SMODS.cull_pool(_p, args) end From 17758101877a7b21913d8bd6512c1ab0f2ffcb5a Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Wed, 8 Apr 2026 11:41:21 +0200 Subject: [PATCH 34/39] Moved patches -/*Title --- lovely/better_calc.toml | 4 +- lovely/blind.toml | 81 +---------------------------------------- 2 files changed, 3 insertions(+), 82 deletions(-) diff --git a/lovely/better_calc.toml b/lovely/better_calc.toml index 09b641102..add5ebadb 100644 --- a/lovely/better_calc.toml +++ b/lovely/better_calc.toml @@ -942,7 +942,7 @@ payload = ''' SMODS.saved = false G.GAME.saved_text = nil SMODS.last_hand = SMODS.last_hand or {scoring_hand = {}, full_hand = {}} -SMODS.calculate_context({end_of_round = true, game_over = game_over, beat_boss = G.GAME.blind:is_type("Boss"), scoring_hand = SMODS.last_hand.scoring_hand, scoring_name = SMODS.last_hand.scoring_name, full_hand = SMODS.last_hand.full_hand }) +SMODS.calculate_context({end_of_round = true, game_over = game_over, beat_boss = G.GAME.blind.boss, scoring_hand = SMODS.last_hand.scoring_hand, scoring_name = SMODS.last_hand.scoring_name, full_hand = SMODS.last_hand.full_hand }) if SMODS.saved then game_over = false end -- TARGET: main end_of_round evaluation ''' @@ -955,7 +955,7 @@ position = 'at' pattern = '''(?[\t ]*)for i=1, #G\.hand\.cards do\n\s+--Check for hand doubling\n(.*\n)*?\s+delay\(0\.3\)''' line_prepend = '$indent' payload = '''for _,v in ipairs(SMODS.get_card_areas('playing_cards', 'end_of_round')) do - SMODS.calculate_end_of_round_effects({ cardarea = v, end_of_round = true, beat_boss = G.GAME.blind:is_type("Boss") }) + SMODS.calculate_end_of_round_effects({ cardarea = v, end_of_round = true, beat_boss = G.GAME.blind.boss }) end ''' diff --git a/lovely/blind.toml b/lovely/blind.toml index a190e6053..f2f916b81 100644 --- a/lovely/blind.toml +++ b/lovely/blind.toml @@ -549,83 +549,4 @@ pattern = "local blindTable = {" position = "after" payload = ''' effect = self.effect, -''' - -### Account for changed Blind:get_type() function -# Blind.set_blind : Use new Blind:is_types() -[[patches]] -[patches.pattern] -target = "blind.lua" -match_indent = true -pattern = "if G.GAME.modifiers.no_blind_reward and G.GAME.modifiers.no_blind_reward[self:get_type()] then self.dollars = 0 end" -position = "at" -payload = '''if G.GAME.modifiers.no_blind_reward and self:is_types(G.GAME.modifiers.no_blind_reward) then self.dollars = 0 end''' -# game.lua : Game:start_run() : account for "Showdown" type with "no_reward" rule -[[patches]] -[patches.pattern] -target = "game.lua" -match_indent = true -pattern = "self.GAME.modifiers.no_blind_reward.Boss = true" -position = "after" -payload = '''self.GAME.modifiers.no_blind_reward.Showdown = true''' -# card.lua : Card:generate_UIBox_ability_table() -[[patches]] -[patches.pattern] -target = "game.lua" -match_indent = true -pattern = "local disableable = G.GAME.blind and ((not G.GAME.blind.disabled) and (G.GAME.blind:get_type() == 'Boss'))" -position = "at" -payload = '''local disableable = G.GAME.blind and ((not G.GAME.blind.disabled) and (G.GAME.blind:is_type("Boss")))''' -# card.lua : Card:calculate_joker() : Luchador !-- could be taken ownership of instead -[[patches]] -[patches.pattern] -target = "game.lua" -match_indent = true -pattern = "if G.GAME.blind and ((not G.GAME.blind.disabled) and (G.GAME.blind:get_type() == 'Boss')) then " -position = "at" -payload = '''if G.GAME.blind and ((not G.GAME.blind.disabled) and (G.GAME.blind:is_type("Boss"))) then ''' -# functions/button_callbacks.lua : G.FUNCS.HUD_blind_reward() -[[patches]] -[patches.pattern] -target = "functions/button_callbacks.lua" -match_indent = true -pattern = "if G.GAME.modifiers.no_blind_reward and (G.GAME.blind and G.GAME.modifiers.no_blind_reward[G.GAME.blind:get_type()]) then" -position = "at" -payload = ''' -if G.GAME.modifiers.no_blind_reward and (G.GAME.blind and blind:is_types(G.GAME.modifiers.no_blind_reward)) then -''' -# functions/state_events.lua : end_round() -[[patches]] -[patches.pattern] -target = "functions/state_events.lua" -match_indent = true -pattern = '''if G.GAME.blind:get_type() == 'Boss' and not G.GAME.seeded and not G.GAME.challenge then''' -position = "at" -payload = ''' -if G.GAME.blind:is_type("Boss") and not G.GAME.seeded and not G.GAME.challenge then -''' -[[patches]] -[patches.pattern] -target = "functions/state_events.lua" -match_indent = true -pattern = '''if G.GAME.blind:get_type() == 'Boss' then''' -position = "at" -payload = ''' -if G.GAME.blind:is_type("Boss") then -''' -[[patches]] -[patches.pattern] -target = "functions/state_events.lua" -match_indent = true -pattern = '''if not G.GAME.won and G.GAME.round_resets.ante >= G.GAME.win_ante and G.GAME.blind_on_deck == 'Boss' then''' -position = "at" -payload = ''' -if G.GAME.round_resets.ante == G.GAME.win_ante and G.GAME.blind:is_type("Boss") then -''' -# game.lua : Game:init_game_object() -[[patches]] -[patches.regex] -target = "game.lua" -pattern = '''if v.boss then bosses_used[k] = 0 end''' -position = "at" -payload = '''if SMODS.get_blind_types(blind_obj).Boss then bosses_used[k] = 0 end''' +''' \ No newline at end of file From 4ffae2a2739982fadd294e5242d499aef0650737 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Wed, 8 Apr 2026 11:45:36 +0200 Subject: [PATCH 35/39] Readded missing `Blind` patch +/*Title --- lovely/blind.toml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/lovely/blind.toml b/lovely/blind.toml index f2f916b81..9d1771fa1 100644 --- a/lovely/blind.toml +++ b/lovely/blind.toml @@ -462,6 +462,44 @@ payload = ''' {n=G.UIT.O, config={object = DynaText({string = localize(target), colours = {G.C.WHITE},shadow = true, float = true,maxw = 2.2, scale = 0.45})}} ''' +# get_new_boss() +[[patches]] +[patches.pattern] +target = 'functions/common_events.lua' +pattern = ''' +if not v.boss then + +elseif not v.boss.showdown and (v.boss.min <= math.max(1, G.GAME.round_resets.ante) and ((math.max(1, G.GAME.round_resets.ante))%G.GAME.win_ante ~= 0 or G.GAME.round_resets.ante < 2)) then + eligible_bosses[k] = true +elseif v.boss.showdown and (G.GAME.round_resets.ante)%G.GAME.win_ante == 0 and G.GAME.round_resets.ante >= 2 then + eligible_bosses[k] = true +end +''' +match_indent = true +position = 'at' +payload = ''' +local res, options = SMODS.add_to_pool(v) +options = options or {} +if not v.boss then + +elseif options.ignore_showdown_check then + eligible_bosses[k] = res and true or nil +elseif v.in_pool and type(v.in_pool) == 'function' then + if + ( + ((G.GAME.round_resets.ante)%G.GAME.win_ante == 0 and G.GAME.round_resets.ante >= 2) == + (v.boss.showdown or false) + ) + then + eligible_bosses[k] = res and true or nil + end +elseif not v.boss.showdown and (v.boss.min <= math.max(1, G.GAME.round_resets.ante) and ((math.max(1, G.GAME.round_resets.ante))%G.GAME.win_ante ~= 0 or G.GAME.round_resets.ante < 2)) then + eligible_bosses[k] = res and true or nil +elseif v.boss.showdown and (G.GAME.round_resets.ante)%G.GAME.win_ante == 0 and G.GAME.round_resets.ante >= 2 then + eligible_bosses[k] = res and true or nil +end +''' + # G.UIDEF.challenge_description_tab [[patches]] [patches.pattern] From e5bbe16327deb0f21cba676982effbd306c448c7 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Thu, 9 Apr 2026 01:11:55 +0200 Subject: [PATCH 36/39] Added `args.leave_HUD_blind` to `BLIND`'s `on_exit()` +Title, currently unused by base code, might be useful though. *Also renamed `leave_sign`->`leave_shop_sign` and added it to the `BOOSTER_OPENED`'s `SMODS.enter_state()` call. --- lovely/booster.toml | 4 ++-- src/game_objects/game_states.lua | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lovely/booster.toml b/lovely/booster.toml index 12586f7ca..50a522de6 100644 --- a/lovely/booster.toml +++ b/lovely/booster.toml @@ -34,7 +34,7 @@ payload = """ booster_obj = self.config.center if booster_obj and SMODS.Centers[booster_obj.key] then SMODS.OPENED_BOOSTER = self - SMODS.enter_state(SMODS.STATES.BOOSTER_OPENED, nil, {from_hold = true}) + SMODS.enter_state(SMODS.STATES.BOOSTER_OPENED, nil, {from_hold = true, leave_shop_sign = true, leave_HUD_blind = true}) end G.GAME.pack_choices = math.min((self.ability.choose or self.config.center.config.choose or 1) + (G.GAME.modifiers.booster_choice_mod or 0), self.ability.extra and math.max(1, self.ability.extra + (G.GAME.modifiers.booster_size_mod or 0)) or self.config.center.extra and math.max(1, self.config.center.extra + (G.GAME.modifiers.booster_size_mod or 0)) or 1) """ @@ -341,7 +341,7 @@ target = 'card.lua' match_indent = true position = 'after' pattern = '''if self.shop_voucher then G.GAME.current_round.voucher = nil end''' -payload = '''SMODS.enter_state(SMODS.STATES.REDEEM_VOUCHER, nil, {from_hold = true, leave_sign = true})''' +payload = '''SMODS.enter_state(SMODS.STATES.REDEEM_VOUCHER, nil, {from_hold = true, leave_shop_sign = true, leave_HUD_blind = true})''' # G.FUNCS.use_card() # Consumables in areas other than the consumable don't count diff --git a/src/game_objects/game_states.lua b/src/game_objects/game_states.lua index c1ba32a3e..0a3d8d92d 100644 --- a/src/game_objects/game_states.lua +++ b/src/game_objects/game_states.lua @@ -413,7 +413,7 @@ SMODS.GameState { if G.shop and not G.shop.alignment.offset.py then G.shop.alignment.offset.py = G.shop.alignment.offset.y G.shop.alignment.offset.y = G.ROOM.T.y + 29 - if not args.leave_sign then + if not args.leave_shop_sign then G.SHOP_SIGN.alignment.offset.y = -15 end end @@ -630,7 +630,7 @@ SMODS.GameState { on_exit = function (self, args) G.GAME.facing_blind = nil if args.from_hold or args.no_defeat then - if G.HUD_blind then + if G.HUD_blind and not args.leave_HUD_blind then G.HUD_blind.alignment.offset.py = G.HUD_blind.alignment.offset.y G.HUD_blind.alignment.offset.y = -10 end From bcae37d05d69e717be19a143a837c6fb733a2336 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Sun, 17 May 2026 13:50:12 +0200 Subject: [PATCH 37/39] Generalized `SMODS.get_card_by_sort_id` to find any card. */+Title --- src/game_objects/game_states.lua | 4 ++-- src/utils.lua | 26 ++++++++++++++++---------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/game_objects/game_states.lua b/src/game_objects/game_states.lua index 0a3d8d92d..b2db7575a 100644 --- a/src/game_objects/game_states.lua +++ b/src/game_objects/game_states.lua @@ -575,8 +575,8 @@ SMODS.GameState { G.GAME.current_round.hands_played = data.hands_played ease_discard(G.GAME.current_round.discards_left - data.discards_left) G.GAME.current_round.discards_used = data.discards_used - local hand_cards = SMODS.get_cards_by_sort_ids(data.hand_cards) - local discarded_cards = SMODS.get_cards_by_sort_ids(data.discarded_cards) + local hand_cards = SMODS.get_cards_by_sort_ids(data.hand_cards, {G.playing_cards}) + local discarded_cards = SMODS.get_cards_by_sort_ids(data.discarded_cards, {G.playing_cards}) for _, pcard_sort_id in ipairs(data.hand_cards) do local pcard = hand_cards[pcard_sort_id] if pcard and not pcard.removed then diff --git a/src/utils.lua b/src/utils.lua index 5681aa48d..9b6bc29f7 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -4137,15 +4137,19 @@ function SMODS.create_unlock_text(center) return localize('k_'..string.lower(center and center.set or 'unknown')) end -function SMODS.get_card_by_sort_id(sort_id) - for _, pcard in ipairs(G.playing_cards) do - if pcard.sort_id == sort_id then - return pcard - end - end +-- Util function to get a single card by its sort_id. +function SMODS.get_card_by_sort_id(sort_id, card_lists) + card_lists = card_lists or {(G.jokers or {}).cards or {}, (G.consumeables or {}).cards or {}, G.playing_cards} + for _, cards in ipairs(card_lists) do + for _, card in ipairs(cards) do + if card.sort_id == sort_id then return card end + end + end end -function SMODS.get_cards_by_sort_ids(sort_ids) +-- Util function to get multiple cards by their sort_ids. +function SMODS.get_cards_by_sort_ids(sort_ids, card_lists) + card_lists = card_lists or {(G.jokers or {}).cards or {}, (G.consumeables or {}).cards or {}, G.playing_cards} local sort_ids_map = {} if sort_ids[1] and type(sort_ids[1]) == "number" then for _, sort_id in ipairs(sort_ids) do @@ -4155,9 +4159,11 @@ function SMODS.get_cards_by_sort_ids(sort_ids) sort_ids_map = sort_ids end local ret = {} - for _, pcard in ipairs(G.playing_cards) do - if sort_ids_map[pcard.sort_id] then - ret[pcard.sort_id] = pcard + for _, cards in ipairs(card_lists) do + for _, card in ipairs(cards) do + if sort_ids_map[card.sort_id] then + ret[card.sort_id] = card + end end end return ret From a15ddcd4be659e3bb2d6e27cb1d959c9d87ec4b5 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Tue, 2 Jun 2026 16:06:37 +0200 Subject: [PATCH 38/39] Added `SMODS.STATES.USE_CONSUMABLE` + refactored a bunch +/*Title, there's still some timing/visual issues, especially with using Consumables. --- lovely/booster.toml | 73 ++++--- lovely/center.toml | 8 - lovely/game_state.toml | 46 ++++- lovely/override_notice.toml | 27 ++- src/game_object.lua | 1 - src/game_objects/game_states.lua | 317 +++++++++++++++++-------------- src/overrides.lua | 18 +- src/utils.lua | 4 +- 8 files changed, 291 insertions(+), 203 deletions(-) diff --git a/lovely/booster.toml b/lovely/booster.toml index c1833819f..09bb4a36d 100644 --- a/lovely/booster.toml +++ b/lovely/booster.toml @@ -10,34 +10,53 @@ priority = -10 [patches.pattern] target = "card.lua" pattern = ''' -if self.ability.name:find('Arcana') then - G.STATE = G.STATES.TAROT_PACK - G.GAME.pack_size = self.ability.extra - elseif self.ability.name:find('Celestial') then - G.STATE = G.STATES.PLANET_PACK - G.GAME.pack_size = self.ability.extra - elseif self.ability.name:find('Spectral') then - G.STATE = G.STATES.SPECTRAL_PACK - G.GAME.pack_size = self.ability.extra - elseif self.ability.name:find('Standard') then - G.STATE = G.STATES.STANDARD_PACK - G.GAME.pack_size = self.ability.extra - elseif self.ability.name:find('Buffoon') then - G.STATE = G.STATES.BUFFOON_PACK - G.GAME.pack_size = self.ability.extra - end +if self.ability.name:find('Arcana') then + G.STATE = G.STATES.TAROT_PACK + G.GAME.pack_size = self.ability.extra +elseif self.ability.name:find('Celestial') then + G.STATE = G.STATES.PLANET_PACK + G.GAME.pack_size = self.ability.extra +elseif self.ability.name:find('Spectral') then + G.STATE = G.STATES.SPECTRAL_PACK + G.GAME.pack_size = self.ability.extra +elseif self.ability.name:find('Standard') then + G.STATE = G.STATES.STANDARD_PACK + G.GAME.pack_size = self.ability.extra +elseif self.ability.name:find('Buffoon') then + G.STATE = G.STATES.BUFFOON_PACK + G.GAME.pack_size = self.ability.extra +end - G.GAME.pack_choices = self.config.center.config.choose or 1''' +G.GAME.pack_choices = self.config.center.config.choose or 1 + +if self.cost > 0 then + G.E_MANAGER:add_event(Event({trigger = 'after', delay = 0.2, func = function() + inc_career_stat('c_shop_dollars_spent', self.cost) + self:juice_up() + return true end })) + ease_dollars(-self.cost) +else + delay(0.2) +end''' match_indent = true position = "at" payload = """ +G.GAME.pack_choices = math.min((self.ability.choose or self.config.center.config.choose or 1) + (G.GAME.modifiers.booster_choice_mod or 0), self.ability.extra and math.max(1, self.ability.extra + (G.GAME.modifiers.booster_size_mod or 0)) or self.config.center.extra and math.max(1, self.config.center.extra + (G.GAME.modifiers.booster_size_mod or 0)) or 1) +if self.cost > 0 then + G.E_MANAGER:add_event(Event({trigger = 'after', delay = 0.2, func = function() + inc_career_stat('c_shop_dollars_spent', self.cost) + self:juice_up() + return true end })) + ease_dollars(-self.cost) +else + delay(0.2) +end booster_obj = self.config.center if booster_obj and SMODS.Centers[booster_obj.key] then SMODS.OPENED_BOOSTER = self SMODS.enter_state(SMODS.STATES.BOOSTER_OPENED, nil, {from_hold = true, leave_shop_sign = true, leave_HUD_blind = true}) end -G.GAME.pack_choices = math.min((self.ability.choose or self.config.center.config.choose or 1) + (G.GAME.modifiers.booster_choice_mod or 0), self.ability.extra and math.max(1, self.ability.extra + (G.GAME.modifiers.booster_size_mod or 0)) or self.config.center.extra and math.max(1, self.config.center.extra + (G.GAME.modifiers.booster_size_mod or 0)) or 1) -""" +""" # Reordered to maintain vanilla timing # Card:open # Adds modifier for size of booster @@ -105,7 +124,7 @@ target = "cardarea.lua" pattern = "(self.config.type == 'deck' and self ~= G.deck) or" position = "before" payload = ''' -(self.config.type == 'hand' and state == SMODS.STATES.BOOSTER_OPENED) or''' +(self.config.type == 'hand' and SMODS.in_booster()) or''' match_indent = true # G.FUNCS.use_card @@ -124,7 +143,7 @@ match_indent = true target = "cardarea.lua" pattern = "if self.config.type == 'hand' and (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.PLANET_PACK) then" position = "at" -payload = "if self.config.type == 'hand' and (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED) then" +payload = "if self.config.type == 'hand' and (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.PLANET_PACK or SMODS.in_booster()) then" match_indent = true # CardArea:align_cards() @@ -133,7 +152,7 @@ match_indent = true target = "cardarea.lua" pattern = "if self.config.type == 'hand' and not (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.PLANET_PACK) then" position = "at" -payload = "if self.config.type == 'hand' and not (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED) then" +payload = "if self.config.type == 'hand' and not (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.PLANET_PACK or SMODS.in_booster()) then" match_indent = true # Card:can_use_consumable() @@ -157,7 +176,7 @@ if nc then play_sound('cardSlide2', nil, 0.3) dont_dissolve = true end -if (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED) then""" +if (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.SPECTRAL_PACK or SMODS.in_booster()) then""" match_indent = true # G.FUNC.use_card() @@ -197,7 +216,7 @@ position = "before" payload = "(G.STATE == SMODS.STATES.BOOSTER_OPENED and SMODS.STATES.BOOSTER_OPENED) or" match_indent = true -# G.FUNC.use_card() +# G.FUNC.draw_from_deck_to_hand() [[patches]] [patches.pattern] target = "functions/state_events.lua" @@ -213,8 +232,8 @@ target = "card.lua" pattern = '''(?[\t ]*)align = \(G\.STATE[\s\S]*and -0\.2 or 0},''' position = "at" payload = ''' -align = (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED) and 'tm' or 'cm', -offset = {x = 0, y = (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == SMODS.STATES.BOOSTER_OPENED) and -0.2 or 0},''' +align = (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or SMODS.in_booster()) and 'tm' or 'cm', +offset = {x = 0, y = (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or SMODS.in_booster()) and -0.2 or 0},''' line_prepend = '$indent' # G.FUNCS.use_card() @@ -436,7 +455,7 @@ target = 'card.lua' match_indent = true position = 'after' pattern = '''if self.shop_voucher then G.GAME.current_round.voucher = nil end''' -payload = '''SMODS.enter_state(SMODS.STATES.REDEEM_VOUCHER, nil, {from_hold = true, leave_shop_sign = true, leave_HUD_blind = true})''' +payload = '''SMODS.enter_state(SMODS.STATES.REDEEM_VOUCHER, nil, {from_hold = true, leave_shop_sign = true, leave_HUD_blind = true, leave_data = true, no_sound = true})''' # G.FUNCS.use_card() # Consumables in areas other than the consumable don't count diff --git a/lovely/center.toml b/lovely/center.toml index 47e970c81..4fb183f8e 100644 --- a/lovely/center.toml +++ b/lovely/center.toml @@ -553,14 +553,6 @@ match_indent = true position = 'at' payload = '''elseif not nc then draw_card(G.hand, G.play, 1, 'up', true, card, nil, mute) end''' -[[patches]] -[patches.pattern] -target = 'functions/button_callbacks.lua' -pattern = "else draw_card(G.hand, G.play, 1, 'up', true, card, nil, mute) end" -match_indent = true -position = 'at' -payload = '''elseif not nc then draw_card(G.hand, G.play, 1, 'up', true, card, nil, mute) end''' - # Card:check_use - allow Ankh to be selectable from packs even if joker slots are full [[patches]] [patches.pattern] diff --git a/lovely/game_state.toml b/lovely/game_state.toml index a9c4a0f4a..f3a49d13d 100644 --- a/lovely/game_state.toml +++ b/lovely/game_state.toml @@ -78,12 +78,8 @@ payload = '''(self.config.type == 'hand' and ({[SMODS.STATES.SHOP]=1, [G.STATES. target = 'functions/button_callbacks.lua' match_indent = true position = 'at' -pattern = ''' -if card.ability.set == 'Booster' and not nosave and G.STATE == G.STATES.SHOP -''' -payload = ''' -if card.ability.set == 'Booster' and not nosave and G.STATE == SMODS.STATES.SHOP -''' +pattern = '''if card.ability.set == 'Booster' and not nosave and G.STATE == G.STATES.SHOP then''' +payload = '''if card.ability.set == 'Booster' and not nosave and G.STATE == SMODS.STATES.SHOP then''' [[patches]] [patches.pattern] target = 'functions/button_callbacks.lua' @@ -136,7 +132,7 @@ position = 'at' pattern = '''G.STATE = prev_state''' payload = ''' if SMODS.GameStates[G.STATE] and SMODS.GameStates[G.STATE].exit_after_use_card then -- SMODS.STATES.BOOSTER_OPENED is handled by G.FUNCS.end_consumeable() below - SMODS.exit_state(nil, nil, {state_override=prev_state}) + SMODS.exit_state(nil, {no_shop_calc = true, leave_data = true, no_sound = true}, {state_override=prev_state}) elseif not SMODS.GameStates[G.STATE] then G.STATE = prev_state end @@ -173,7 +169,7 @@ position = 'at' pattern = '''G.STATE = G.GAME.PACK_INTERRUPT''' payload = ''' if SMODS.GameStates[G.STATE] and SMODS.GameStates[G.STATE].exit_after_end_consumable then - SMODS.exit_state(nil, nil, {state_override=G.GAME.PACK_INTERRUPT}) + SMODS.exit_state(nil, {no_shop_calc = true, no_sound = true}, {state_override=G.GAME.PACK_INTERRUPT}) elseif not SMODS.GameStates[G.STATE] then G.STATE = G.GAME.PACK_INTERRUPT end @@ -229,4 +225,36 @@ payload = ''' win_game() G.GAME.won = true return true -''' \ No newline at end of file +''' + + +### SMODS.STATES.USE_CONSUMABLE + +# card.lua : Card:use_consumable() : inject SMODS.enter_state +[[patches]] +[patches.pattern] +target = 'card.lua' +match_indent = true +position = 'after' +pattern = '''local used_tarot = copier or self''' +payload = ''' +SMODS.enter_state(SMODS.STATES.USE_CONSUMABLE, nil, {from_hold = true, leave_shop_sign = true, leave_HUD_blind = true, leave_data = true, no_sound = true}) +''' + +# card.lua : Card:can_use_consumeable() : update logic +[[patches]] +[patches.pattern] +target = 'card.lua' +match_indent = true +position = 'at' +pattern = '''if G.STATE ~= G.STATES.HAND_PLAYED and G.STATE ~= G.STATES.DRAW_TO_HAND and G.STATE ~= G.STATES.PLAY_TAROT or any_state then''' +payload = '''if G.STATE ~= G.STATES.HAND_PLAYED and G.STATE ~= G.STATES.DRAW_TO_HAND and G.STATE ~= SMODS.STATES.USE_CONSUMABLE or any_state then''' + +# functions/button_callbacks.lua : G.FUNCS.use_card() : update logic +[[patches]] +[patches.pattern] +target = 'functions/button_callbacks.lua' +match_indent = true +position = 'at' +pattern = '''G.STATES.PLAY_TAROT''' +payload = '''SMODS.STATES.USE_CONSUMABLE''' \ No newline at end of file diff --git a/lovely/override_notice.toml b/lovely/override_notice.toml index b52820cc0..b439a4de1 100644 --- a/lovely/override_notice.toml +++ b/lovely/override_notice.toml @@ -225,6 +225,31 @@ position = "before" payload = "-- Function overridden by SMODS in src/overrides.lua" match_indent = true +[[patches]] +[patches.pattern] +target = "functions/button_callbacks.lua" +pattern = "G.FUNCS.toggle_shop = function(e)" +position = "before" +payload = "-- Function overridden by SMODS in src/overrides.lua" +match_indent = true + +[[patches]] +[patches.pattern] +target = "functions/button_callbacks.lua" +pattern = "G.FUNCS.cash_out = function(e)" +position = "before" +payload = "-- Function overridden by SMODS in src/overrides.lua" +match_indent = true + +[[patches]] +[patches.pattern] +target = "functions/button_callbacks.lua" +pattern = "G.FUNCS.select_blind = function(e)" +position = "before" +payload = "-- Function overridden by SMODS in src/overrides.lua" +match_indent = true + + # src/ui.lua [[patches]] @@ -281,4 +306,4 @@ target = "functions/UI_definitions.lua" pattern = "function G.UIDEF.custom_deck_tab(_suit)" position = "before" payload = "-- Function overridden by SMODS in src/ui.lua" -match_indent = true +match_indent = true \ No newline at end of file diff --git a/src/game_object.lua b/src/game_object.lua index 5e1662fa8..8408f079a 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -1524,7 +1524,6 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. --]] update_pack = function(self, dt) if G.buttons then G.buttons:remove(); G.buttons = nil end - if G.shop then G.shop.alignment.offset.y = G.ROOM.T.y+11 end if not G.STATE_COMPLETE then G.STATE_COMPLETE = true diff --git a/src/game_objects/game_states.lua b/src/game_objects/game_states.lua index b2db7575a..e28cd54c5 100644 --- a/src/game_objects/game_states.lua +++ b/src/game_objects/game_states.lua @@ -1,6 +1,7 @@ SMODS.STATES = { BOOSTER_OPENED = "BOOSTER_OPENED", REDEEM_VOUCHER = "REDEEM_VOUCHER", + USE_CONSUMABLE = "USE_CONSUMABLE", SHOP = "SHOP", ROUND_EVAL = "ROUND_EVAL", BLIND = "BLIND", @@ -42,16 +43,16 @@ function SMODS.get_next_held_state(allow_duplicate) return nil end -function SMODS.is_state_in_stack(state, exclude_latest) +function SMODS.get_state_stack_index(state, exclude_latest) for i, state_table in ipairs(SMODS.state_stack) do if exclude_latest and i == #SMODS.state_stack then - return false + return nil end if state_table.state == state then - return true + return i end end - return false + return nil end function SMODS.clear_state_stack() @@ -64,14 +65,16 @@ function SMODS.enter_state(new_state, enter_args, exit_args) local next_held_state = SMODS.get_next_held_state() enter_args = enter_args or {} enter_args.old_state = current_state - if SMODS.is_state_in_stack(new_state) then + local new_state_index = SMODS.get_state_stack_index(new_state) + if new_state_index then if not enter_args.force_refresh then enter_args.from_hold = true end end exit_args = exit_args or {} exit_args.new_state = exit_args.new_state or new_state - local exit_state_in_stack = not exit_args.from_hold and SMODS.is_state_in_stack(current_state, true) -- If exit_args.from_hold is already true, this is irrelevant + local current_state_index = SMODS.get_state_stack_index(current_state, true) + local exit_state_in_stack = not exit_args.from_hold and current_state_index -- If exit_args.from_hold is already true, this is irrelevant if exit_state_in_stack then exit_args.from_hold = true end @@ -104,12 +107,12 @@ function SMODS.exit_state(exit_args, enter_args, default) end exit_args = exit_args or {} exit_args.new_state = exit_args.new_state or new_state - if SMODS.is_state_in_stack(current_state, true) then + if SMODS.get_state_stack_index(current_state, true) then exit_args.from_hold = true end enter_args = enter_args or {} enter_args.old_state = current_state - if SMODS.is_state_in_stack(new_state) then + if SMODS.get_state_stack_index(new_state) then if not enter_args.force_refresh then enter_args.from_hold = true end @@ -202,9 +205,24 @@ SMODS.GameState { exit_after_end_consumable = true, } +function SMODS.in_booster() + local held_state = SMODS.get_next_held_state() + return SMODS.STATE == SMODS.STATES.BOOSTER_OPENED or ((SMODS.GameStates[SMODS.STATE] or {}).holds_booster and held_state == SMODS.STATES.BOOSTER_OPENED) +end + SMODS.GameState { key = SMODS.STATES.REDEEM_VOUCHER, exit_after_use_card = true, + holds_booster = true, +} + +SMODS.GameState { + key = SMODS.STATES.USE_CONSUMABLE, + on_enter = function (self, args) + if G.buttons then G.buttons:remove(); G.buttons = nil end + end, + exit_after_use_card = true, + holds_booster = true, } SMODS.GameState { @@ -253,9 +271,10 @@ SMODS.GameState { end end, on_enter = function (self, args) + args = args or {} G.CONTROLLER.locks.toggle_shop = nil if args.force_refresh then - self:on_exit({}) + self:on_exit() elseif args.from_hold then if G.shop then -- Extracted from G.FUNCS.use_card() @@ -263,23 +282,24 @@ SMODS.GameState { G.shop.alignment.offset.py = nil G.SHOP_SIGN.alignment.offset.y = 0 - G.E_MANAGER:add_event(Event({ - trigger = 'after', - delay = 0.2, - blockable = false, - func = function() - if math.abs(G.shop.T.y - G.shop.VT.y) < 3 then - G.ROOM.jiggle = G.ROOM.jiggle + 3 - play_sound('cardFan2') - for i = 1, #G.GAME.tags do - G.GAME.tags[i]:apply_to_run({type = 'shop_start'}) + if not args.no_shop_calc then + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 0.2, + blockable = false, + func = function() + if math.abs(G.shop.T.y - G.shop.VT.y) < 3 then + G.ROOM.jiggle = G.ROOM.jiggle + 3 + if not args.no_sound then play_sound('cardFan2') end + for i = 1, #G.GAME.tags do + G.GAME.tags[i]:apply_to_run({type = 'shop_start'}) + end + return true end - return true end - end - })) - - SMODS.calculate_context({starting_shop = true, from_hold = true}) + })) + SMODS.calculate_context({starting_shop = true, from_hold = true}) + end G.CONTROLLER:snap_to({node = G.shop:get_UIE_by_ID('next_round_button')}) G.E_MANAGER:add_event(Event({ func = function() save_run(); return true end})) else @@ -288,119 +308,113 @@ SMODS.GameState { end return end + stop_use() + G.STATE_COMPLETE = true + ease_background_colour_blind(G.STATES.SHOP) + local shop_exists = not not G.shop + G.shop = G.shop or UIBox{ + definition = G.UIDEF.shop(), + config = {align='tmi', offset = {x=0,y=G.ROOM.T.y+11},major = G.hand, bond = 'Weak'} + } + -- Moved here from G.FUNCS.cash_out() + G.GAME.current_round.jokers_purchased = 0 + G.GAME.shop_free = nil + G.GAME.shop_d6ed = nil + ------- G.E_MANAGER:add_event(Event({ - trigger = "immediate", - func = function () - stop_use() - G.STATE_COMPLETE = true - ease_background_colour_blind(G.STATES.SHOP) - local shop_exists = not not G.shop - G.shop = G.shop or UIBox{ - definition = G.UIDEF.shop(), - config = {align='tmi', offset = {x=0,y=G.ROOM.T.y+11},major = G.hand, bond = 'Weak'} - } - -- Moved here from G.FUNCS.cash_out() - G.GAME.current_round.jokers_purchased = 0 - G.GAME.shop_free = nil - G.GAME.shop_d6ed = nil - ------- + func = function() + G.shop.alignment.offset.y = -5.3 + G.shop.alignment.offset.x = 0 G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 0.2, + blockable = false, func = function() - G.shop.alignment.offset.y = -5.3 - G.shop.alignment.offset.x = 0 - G.E_MANAGER:add_event(Event({ - trigger = 'after', - delay = 0.2, - blockable = false, - func = function() - if math.abs(G.shop.T.y - G.shop.VT.y) < 3 then - G.ROOM.jiggle = G.ROOM.jiggle + 3 - play_sound('cardFan2') - for i = 1, #G.GAME.tags do - G.GAME.tags[i]:apply_to_run({type = 'shop_start'}) + if math.abs(G.shop.T.y - G.shop.VT.y) < 3 then + G.ROOM.jiggle = G.ROOM.jiggle + 3 + if not args.no_sound then play_sound('cardFan2') end + for i = 1, #G.GAME.tags do + G.GAME.tags[i]:apply_to_run({type = 'shop_start'}) + end + local nosave_shop = nil + if not shop_exists then + if G.load_shop_jokers then + nosave_shop = true + G.shop_jokers:load(G.load_shop_jokers) + for k, v in ipairs(G.shop_jokers.cards) do + create_shop_card_ui(v) + if v.ability.consumeable then v:start_materialize() end + for _kk, vvv in ipairs(G.GAME.tags) do + if vvv:apply_to_run({type = 'store_joker_modify', card = v}) then break end + end end - local nosave_shop = nil - if not shop_exists then - if G.load_shop_jokers then - nosave_shop = true - G.shop_jokers:load(G.load_shop_jokers) - for k, v in ipairs(G.shop_jokers.cards) do - create_shop_card_ui(v) - if v.ability.consumeable then v:start_materialize() end - for _kk, vvv in ipairs(G.GAME.tags) do - if vvv:apply_to_run({type = 'store_joker_modify', card = v}) then break end - end - end - G.load_shop_jokers = nil - else - for i = 1, G.GAME.shop.joker_max - #G.shop_jokers.cards do - G.shop_jokers:emplace(create_card_for_shop(G.shop_jokers)) - end + G.load_shop_jokers = nil + else + for i = 1, G.GAME.shop.joker_max - #G.shop_jokers.cards do + G.shop_jokers:emplace(create_card_for_shop(G.shop_jokers)) + end + end + + if G.load_shop_vouchers then + nosave_shop = true + G.shop_vouchers:load(G.load_shop_vouchers) + for k, v in ipairs(G.shop_vouchers.cards) do + create_shop_card_ui(v) + v:start_materialize() + end + G.load_shop_vouchers = nil + else + local vouchers_to_spawn = 0 + for _,_ in pairs(G.GAME.current_round.voucher.spawn) do vouchers_to_spawn = vouchers_to_spawn + 1 end + if vouchers_to_spawn < G.GAME.starting_params.vouchers_in_shop + (G.GAME.modifiers.extra_vouchers or 0) then + SMODS.get_next_vouchers(G.GAME.current_round.voucher) + end + for _, key in ipairs(G.GAME.current_round.voucher or {}) do + if G.P_CENTERS[key] and G.GAME.current_round.voucher.spawn[key] then + SMODS.add_voucher_to_shop(key) end + end + end - if G.load_shop_vouchers then - nosave_shop = true - G.shop_vouchers:load(G.load_shop_vouchers) - for k, v in ipairs(G.shop_vouchers.cards) do - create_shop_card_ui(v) - v:start_materialize() - end - G.load_shop_vouchers = nil - else - local vouchers_to_spawn = 0 - for _,_ in pairs(G.GAME.current_round.voucher.spawn) do vouchers_to_spawn = vouchers_to_spawn + 1 end - if vouchers_to_spawn < G.GAME.starting_params.vouchers_in_shop + (G.GAME.modifiers.extra_vouchers or 0) then - SMODS.get_next_vouchers(G.GAME.current_round.voucher) - end - for _, key in ipairs(G.GAME.current_round.voucher or {}) do - if G.P_CENTERS[key] and G.GAME.current_round.voucher.spawn[key] then - SMODS.add_voucher_to_shop(key) - end - end + if G.load_shop_booster then + nosave_shop = true + G.shop_booster:load(G.load_shop_booster) + for k, v in ipairs(G.shop_booster.cards) do + create_shop_card_ui(v) + v:start_materialize() + end + G.load_shop_booster = nil + else + for i=1, G.GAME.starting_params.boosters_in_shop + (G.GAME.modifiers.extra_boosters or 0) do + G.GAME.current_round.used_packs = G.GAME.current_round.used_packs or {} + if not G.GAME.current_round.used_packs[i] then + G.GAME.current_round.used_packs[i] = get_pack('shop_pack').key end - if G.load_shop_booster then - nosave_shop = true - G.shop_booster:load(G.load_shop_booster) - for k, v in ipairs(G.shop_booster.cards) do - create_shop_card_ui(v) - v:start_materialize() - end - G.load_shop_booster = nil - else - for i=1, G.GAME.starting_params.boosters_in_shop + (G.GAME.modifiers.extra_boosters or 0) do - G.GAME.current_round.used_packs = G.GAME.current_round.used_packs or {} - if not G.GAME.current_round.used_packs[i] then - G.GAME.current_round.used_packs[i] = get_pack('shop_pack').key - end - - if G.GAME.current_round.used_packs[i] ~= 'USED' then - local card = Card(G.shop_booster.T.x + G.shop_booster.T.w/2, - G.shop_booster.T.y, G.CARD_W*1.27, G.CARD_H*1.27, G.P_CARDS.empty, G.P_CENTERS[G.GAME.current_round.used_packs[i]], {bypass_discovery_center = true, bypass_discovery_ui = true}) - create_shop_card_ui(card, 'Booster', G.shop_booster) - card.ability.booster_pos = i - card:start_materialize() - G.shop_booster:emplace(card) - end - end - - for i = 1, #G.GAME.tags do - G.GAME.tags[i]:apply_to_run({type = 'voucher_add'}) - end - for i = 1, #G.GAME.tags do - G.GAME.tags[i]:apply_to_run({type = 'shop_final_pass'}) - end + if G.GAME.current_round.used_packs[i] ~= 'USED' then + local card = Card(G.shop_booster.T.x + G.shop_booster.T.w/2, + G.shop_booster.T.y, G.CARD_W*1.27, G.CARD_H*1.27, G.P_CARDS.empty, G.P_CENTERS[G.GAME.current_round.used_packs[i]], {bypass_discovery_center = true, bypass_discovery_ui = true}) + create_shop_card_ui(card, 'Booster', G.shop_booster) + card.ability.booster_pos = i + card:start_materialize() + G.shop_booster:emplace(card) end end - if not nosave_shop then SMODS.calculate_context({starting_shop = true}) end - G.CONTROLLER:snap_to({node = G.shop:get_UIE_by_ID('next_round_button')}) - if not nosave_shop then G.E_MANAGER:add_event(Event({ func = function() save_run(); return true end})) end - return true + for i = 1, #G.GAME.tags do + G.GAME.tags[i]:apply_to_run({type = 'voucher_add'}) + end + for i = 1, #G.GAME.tags do + G.GAME.tags[i]:apply_to_run({type = 'shop_final_pass'}) + end end end - })) - return true + + if not nosave_shop then SMODS.calculate_context({starting_shop = true}) end + G.CONTROLLER:snap_to({node = G.shop:get_UIE_by_ID('next_round_button')}) + if not nosave_shop then G.E_MANAGER:add_event(Event({ func = function() save_run(); return true end})) end + return true + end end })) return true @@ -408,6 +422,7 @@ SMODS.GameState { })) end, on_exit = function (self, args) + args = args or {} G.CONTROLLER.locks.toggle_shop = true if args.from_hold then if G.shop and not G.shop.alignment.offset.py then @@ -460,8 +475,9 @@ SMODS.GameState { SMODS.GameState { key = SMODS.STATES.ROUND_EVAL, on_enter = function (self, args) + args = args or {} if args.force_refresh then - self:on_exit({}) + self:on_exit() elseif args.from_hold then if G.round_eval then G.round_eval.alignment.offset.y = G.round_eval.alignment.offset.py @@ -510,6 +526,7 @@ SMODS.GameState { })) end, on_exit = function (self, args) + args = args or {} if args.from_hold then if G.round_eval and not G.round_eval.alignment.offset.py then G.round_eval.alignment.offset.py = G.round_eval.alignment.offset.y @@ -562,10 +579,17 @@ SMODS.GameState { SMODS.GameState { key = SMODS.STATES.BLIND, on_enter = function (self, args) + args = args or {} if args.force_refresh then - self:on_exit({}) + self:on_exit() elseif args.from_hold then G.GAME.facing_blind = true + if args.leave_data then + save_run() + G.STATE = G.STATES.SELECTING_HAND + G.CONTROLLER:recall_cardarea_focus('hand') + return + end local data = SMODS.state_stack[#SMODS.state_stack].data ease_chips(data.chips) G.GAME.blind:set_blind(G.P_BLINDS[data.blind_key], nil, true) @@ -628,6 +652,7 @@ SMODS.GameState { })) end, on_exit = function (self, args) + args = args or {} G.GAME.facing_blind = nil if args.from_hold or args.no_defeat then if G.HUD_blind and not args.leave_HUD_blind then @@ -658,23 +683,25 @@ SMODS.GameState { end SMODS.state_stack[#SMODS.state_stack].data = data end - G.FUNCS.draw_from_hand_to_discard() - G.FUNCS.draw_from_discard_to_deck() - G.E_MANAGER:add_event(Event({ - trigger = "after", - blockable = false, - delay = 0.7, - func = function () - G.GAME.blind:disable() - if G.buttons then G.buttons:remove(); G.buttons = nil end - return true + if not args.leave_data then + G.FUNCS.draw_from_hand_to_discard() + G.FUNCS.draw_from_discard_to_deck() + G.E_MANAGER:add_event(Event({ + trigger = "after", + blockable = false, + delay = 0.7, + func = function () + G.GAME.blind:disable() + if G.buttons then G.buttons:remove(); G.buttons = nil end + return true + end + })) + for k, v in ipairs(G.playing_cards) do + v.ability.discarded = nil + v.ability.forced_selection = nil end - })) - for k, v in ipairs(G.playing_cards) do - v.ability.discarded = nil - v.ability.forced_selection = nil + delay(0.4) end - delay(0.4) return end G.E_MANAGER:add_event(Event({ @@ -688,6 +715,7 @@ SMODS.GameState { func = function () G.E_MANAGER:add_event(Event({ trigger = 'before', + blocking = false, delay = 1.3*math.min(G.GAME.blind.dollars+2, 7)/2*0.15 + 0.5, func = function() G.GAME.blind:defeat() @@ -706,6 +734,7 @@ SMODS.GameState { G.E_MANAGER:add_event(Event({ trigger = "after", blockable = false, + blocking = false, delay = 0.7, func = function () G.GAME.blind:defeat() @@ -743,8 +772,9 @@ SMODS.GameState { SMODS.GameState { key = SMODS.STATES.BLIND_SELECT, on_enter = function (self, args) + args = args or {} if args.force_refresh then - self:on_exit({}) + self:on_exit() elseif args.from_hold then if G.blind_select then G.blind_select.alignment.offset.y = G.blind_select.alignment.offset.py @@ -753,7 +783,7 @@ SMODS.GameState { trigger = "after", delay = 0.3, func = function () - play_sound('cancel') + if not args.no_sound then play_sound('cancel') end return true end })) @@ -799,6 +829,7 @@ SMODS.GameState { })) end, on_exit = function (self, args) + args = args or {} if args.from_hold then if G.blind_select and not G.blind_select.alignment.offset.py then G.blind_select.alignment.offset.py = G.blind_select.alignment.offset.y diff --git a/src/overrides.lua b/src/overrides.lua index 9662f91bb..7334d1684 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -2885,20 +2885,14 @@ function G.FUNCS.toggle_shop(e) end function G.FUNCS.cash_out(e) - stop_use() + --stop_use() if G.round_eval then e.config.button = nil - G.E_MANAGER:add_event(Event({ - trigger = 'immediate', - func = function() - if #SMODS.state_queue == 0 then - SMODS.queue_state(SMODS.STATES.SHOP) - end - SMODS.advance_state_queue() - G.STATE_COMPLETE = false - return true - end - })) + if #SMODS.state_queue == 0 then + SMODS.queue_state(SMODS.STATES.SHOP) + end + SMODS.advance_state_queue() + G.STATE_COMPLETE = false end end diff --git a/src/utils.lua b/src/utils.lua index 8d3007777..3eab66d15 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -1325,7 +1325,7 @@ SMODS.calculate_individual_effect = function(effect, scored_card, key, amount, f money_altered = true, amount = amount, from_shop = (G.STATE == SMODS.STATES.SHOP or G.STATE == SMODS.STATES.BOOSTER_OPENED or G.STATE == SMODS.STATES.REDEEM_VOUCHER) or nil, - from_consumeable = (G.STATE == G.STATES.PLAY_TAROT) or nil, + from_consumeable = (G.STATE == SMODS.STATES.USE_CONSUMABLE) or nil, from_scoring = (G.STATE == G.STATES.HAND_PLAYED) or nil, }) return true @@ -3390,7 +3390,7 @@ function ease_dollars(mod, instant) money_altered = true, amount = mod, from_shop = (G.STATE == SMODS.STATES.SHOP or G.STATE == SMODS.STATES.BOOSTER_OPENED or G.STATE == SMODS.STATES.REDEEM_VOUCHER) or nil, - from_consumeable = (G.STATE == G.STATES.PLAY_TAROT) or nil, + from_consumeable = (G.STATE == SMODS.STATES.USE_CONSUMABLE) or nil, from_scoring = (G.STATE == G.STATES.HAND_PLAYED) or nil, }) end From 757099b08e6fe396748e05dee80bd5beef124ee1 Mon Sep 17 00:00:00 2001 From: AllUniversal Date: Wed, 3 Jun 2026 01:22:31 +0200 Subject: [PATCH 39/39] WIP Fix for double `end_round()` call *Title --- lovely/game_state.toml | 5 ++- src/game_objects/game_states.lua | 61 +++++++++++++++----------------- src/overrides.lua | 1 - 3 files changed, 33 insertions(+), 34 deletions(-) diff --git a/lovely/game_state.toml b/lovely/game_state.toml index f3a49d13d..a70724547 100644 --- a/lovely/game_state.toml +++ b/lovely/game_state.toml @@ -202,7 +202,10 @@ payload = ''' target = 'functions/state_events.lua' match_indent = true position = 'at' -pattern = '''G.STATE = G.STATES.ROUND_EVAL''' +pattern = ''' +G.STATE = G.STATES.ROUND_EVAL +G.STATE_COMPLETE = false +''' payload = ''' if #SMODS.state_queue == 0 then SMODS.queue_state(SMODS.STATES.ROUND_EVAL) diff --git a/src/game_objects/game_states.lua b/src/game_objects/game_states.lua index e28cd54c5..5c7f74ee1 100644 --- a/src/game_objects/game_states.lua +++ b/src/game_objects/game_states.lua @@ -461,7 +461,6 @@ SMODS.GameState { G.shop = nil G.SHOP_SIGN:remove() G.SHOP_SIGN = nil - G.STATE_COMPLETE = false G.CONTROLLER.locks.toggle_shop = nil return true end @@ -472,6 +471,12 @@ SMODS.GameState { end, } +local end_round_ref = end_round +function end_round() + if SMODS.STATE ~= SMODS.STATES.BLIND then return end + return end_round_ref() +end + SMODS.GameState { key = SMODS.STATES.ROUND_EVAL, on_enter = function (self, args) @@ -488,39 +493,33 @@ SMODS.GameState { end return end + stop_use() + G.STATE_COMPLETE = true G.E_MANAGER:add_event(Event({ - trigger = "immediate", - func = function () - stop_use() - G.STATE_COMPLETE = true + trigger = 'immediate', + func = function() + save_run() + ease_background_colour_blind(G.STATES.ROUND_EVAL) + G.round_eval = UIBox{ + definition = create_UIBox_round_evaluation(), + config = {align="bm", offset = {x=0,y=G.ROOM.T.y + 19},major = G.hand, bond = 'Weak'} + } + G.round_eval.alignment.offset.x = 0 G.E_MANAGER:add_event(Event({ trigger = 'immediate', func = function() - save_run() - ease_background_colour_blind(G.STATES.ROUND_EVAL) - G.round_eval = UIBox{ - definition = create_UIBox_round_evaluation(), - config = {align="bm", offset = {x=0,y=G.ROOM.T.y + 19},major = G.hand, bond = 'Weak'} - } - G.round_eval.alignment.offset.x = 0 - G.E_MANAGER:add_event(Event({ - trigger = 'immediate', - func = function() - if G.round_eval.alignment.offset.y ~= -7.8 then - G.round_eval.alignment.offset.y = -7.8 - else - if math.abs(G.round_eval.T.y - G.round_eval.VT.y) < 3 then - G.ROOM.jiggle = G.ROOM.jiggle + 3 - play_sound('cardFan2') - delay(0.1) - G.FUNCS.evaluate_round() - return true - end - end - end})) - return true - end - })) + if G.round_eval.alignment.offset.y ~= -7.8 then + G.round_eval.alignment.offset.y = -7.8 + else + if math.abs(G.round_eval.T.y - G.round_eval.VT.y) < 3 then + G.ROOM.jiggle = G.ROOM.jiggle + 3 + play_sound('cardFan2') + delay(0.1) + G.FUNCS.evaluate_round() + return true + end + end + end})) return true end })) @@ -534,7 +533,6 @@ SMODS.GameState { end return end - stop_use() if G.round_eval then G.round_eval.alignment.offset.y = G.ROOM.T.y + 15 G.round_eval.alignment.offset.x = 0 @@ -550,7 +548,6 @@ SMODS.GameState { G.round_eval:remove() G.round_eval = nil end - -- G.STATE_COMPLETE = false return true end })) diff --git a/src/overrides.lua b/src/overrides.lua index 7334d1684..d6a836290 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -2892,7 +2892,6 @@ function G.FUNCS.cash_out(e) SMODS.queue_state(SMODS.STATES.SHOP) end SMODS.advance_state_queue() - G.STATE_COMPLETE = false end end