├── .gitignore ├── .vscode ├── ltex.dictionary.en-US.txt └── settings.json ├── config.lua ├── assets ├── 1x │ └── modicon.png └── 2x │ └── modicon.png ├── lovely ├── misc_functions.toml ├── ui_definitions.toml ├── button_callbacks.toml ├── card.toml ├── cardarea.toml ├── button_callbacks.lua ├── ui.toml ├── ui_definitions.lua ├── controller.toml └── misc_functions.lua ├── touch-mode.json ├── main.lua └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.lovelyignore 2 | -------------------------------------------------------------------------------- /.vscode/ltex.dictionary.en-US.txt: -------------------------------------------------------------------------------- 1 | Balatro 2 | Wiimote 3 | -------------------------------------------------------------------------------- /config.lua: -------------------------------------------------------------------------------- 1 | return { 2 | ["vanilla_joker_sell"] = false 3 | } -------------------------------------------------------------------------------- /assets/1x/modicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramdam/sticky-fingers/HEAD/assets/1x/modicon.png -------------------------------------------------------------------------------- /assets/2x/modicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramdam/sticky-fingers/HEAD/assets/2x/modicon.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "[lua]": { 4 | "editor.formatOnSave": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lovely/misc_functions.toml: -------------------------------------------------------------------------------- 1 | [manifest] 2 | dump_lua = true 3 | priority = 0 4 | version = "1.4.0" 5 | 6 | [[patches]] 7 | [patches.copy] 8 | position = "append" 9 | sources = ["lovely/misc_functions.lua"] 10 | target = "functions/misc_functions.lua" 11 | -------------------------------------------------------------------------------- /lovely/ui_definitions.toml: -------------------------------------------------------------------------------- 1 | [manifest] 2 | dump_lua = true 3 | priority = 0 4 | version = "1.4.0" 5 | 6 | [[patches]] 7 | [patches.copy] 8 | position = "append" 9 | sources = ["lovely/ui_definitions.lua"] 10 | target = "functions/UI_definitions.lua" 11 | -------------------------------------------------------------------------------- /lovely/button_callbacks.toml: -------------------------------------------------------------------------------- 1 | [manifest] 2 | dump_lua = true 3 | priority = 0 4 | version = "1.4.0" 5 | 6 | [[patches]] 7 | [patches.copy] 8 | position = "append" 9 | sources = ["lovely/button_callbacks.lua"] 10 | target = "functions/button_callbacks.lua" 11 | -------------------------------------------------------------------------------- /lovely/card.toml: -------------------------------------------------------------------------------- 1 | [manifest] 2 | dump_lua = true 3 | priority = 0 4 | version = "1.4.0" 5 | 6 | [[patches]] 7 | [patches.pattern] 8 | match_indent = false 9 | pattern = ''' 10 | function Card:can_use_consumeable(any_state, skip_check) 11 | ''' 12 | payload = ''' 13 | function Card:can_use_consumeable(any_state, skip_check) 14 | if not self.ability.consumeable then return false end 15 | ''' 16 | position = "at" 17 | target = "card.lua" 18 | times = 1 19 | -------------------------------------------------------------------------------- /touch-mode.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "eramdam.sticky-fingers", 3 | "name": "Sticky Fingers", 4 | "author": ["Eramdam"], 5 | "description": "Enables mobile-like touch controls to sell/use/select cards", 6 | "prefix": "eramdam.tc", 7 | "main_file": "main.lua", 8 | "priority": 100000, 9 | "badge_colour": "666665", 10 | "badge_text_colour": "FFFFFF", 11 | "display_name": "Sticky Fingers", 12 | "version": "1.4.1", 13 | "dependencies": ["Steamodded (>=1.*)", "Lovely (>=0.6)"] 14 | } 15 | -------------------------------------------------------------------------------- /lovely/cardarea.toml: -------------------------------------------------------------------------------- 1 | [manifest] 2 | dump_lua = true 3 | priority = 0 4 | version = "1.4.0" 5 | 6 | [[patches]] 7 | [patches.pattern] 8 | match_indent = false 9 | pattern = ''' 10 | elseif self.config.type == 'play' or self.config.type == 'shop' or self.config.type == 'consumeable' then 11 | card.states.drag.can = false 12 | ''' 13 | payload = ''' 14 | elseif self.config.type == 'play' or self.config.type == 'shop' or self.config.type == 'consumeable' then 15 | if (self.config.type == 'shop') or (self.config.type == 'consumeable') then 16 | card.states.drag.can = true 17 | else 18 | card.states.drag.can = false 19 | end 20 | ''' 21 | position = "at" 22 | target = "cardarea.lua" 23 | times = 1 24 | -------------------------------------------------------------------------------- /lovely/button_callbacks.lua: -------------------------------------------------------------------------------- 1 | -- Allows compatibility with Talisman 2 | function maybe_to_big(n) 3 | if to_big ~= nil then 4 | return to_big(n) 5 | end 6 | 7 | return n 8 | end 9 | 10 | G.FUNCS.check_drag_target_active = function(e) 11 | if e.config.args.active_check(e.config.args.card) then 12 | if (not e.config.pulse_border) or not e.config.args.init then 13 | e.config.pulse_border = true 14 | e.config.colour = e.config.args.colour 15 | e.config.args.text_colour[4] = 1 16 | e.config.release_func = e.config.args.release_func 17 | end 18 | else 19 | if (e.config.pulse_border) or not e.config.args.init then 20 | e.config.pulse_border = nil 21 | e.config.colour = adjust_alpha(G.C.L_BLACK, 0.9) 22 | e.config.args.text_colour[4] = 0.5 23 | e.config.release_func = nil 24 | end 25 | end 26 | e.config.args.init = true 27 | end 28 | -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | if SMODS.Atlas then 2 | SMODS.Atlas({ 3 | key = "modicon", 4 | path = "modicon.png", 5 | px = 32, 6 | py = 32 7 | }) 8 | end 9 | 10 | 11 | DTM = SMODS.current_mod 12 | 13 | DTM.save_config = function(self) 14 | SMODS.save_mod_config(self) 15 | end 16 | 17 | DTM.config_tab = function() 18 | return { 19 | n = G.UIT.ROOT, 20 | config = { 21 | r = 0.1, minw = 5, align = "cm", padding = 0.2, colour = G.C.BLACK 22 | }, 23 | nodes = { 24 | create_toggle({ 25 | id = "vanilla_joker_sell", 26 | label = "Vanilla Joker sell target", 27 | info = {"Use the mobile Joker sell target. Beware of accidental sells!"}, 28 | ref_table = DTM.config, 29 | ref_value = "vanilla_joker_sell", 30 | callback = function() 31 | DTM:save_config() 32 | end, 33 | }) 34 | } 35 | } 36 | end 37 | -------------------------------------------------------------------------------- /lovely/ui.toml: -------------------------------------------------------------------------------- 1 | [manifest] 2 | dump_lua = true 3 | priority = 0 4 | version = "1.4.0" 5 | 6 | [[patches]] 7 | [patches.pattern] 8 | match_indent = false 9 | pattern = ''' 10 | self.ARGS.button_colours[2] = (((collided_button.config.hover and collided_button.states.hover.is) or (collided_button.last_clicked and collided_button.last_clicked > G.TIMERS.REAL - 0.1)) and G.C.UI.HOVER or nil) 11 | ''' 12 | payload = ''' 13 | self.ARGS.button_colours[2] = (((collided_button.config.hover and collided_button.states.hover.is and ((not G.CONTROLLER.HID.touch) or G.CONTROLLER.is_cursor_down)) or (collided_button.last_clicked and collided_button.last_clicked > G.TIMERS.REAL - 0.1)) and G.C.UI.HOVER or nil) 14 | ''' 15 | position = "at" 16 | target = "engine/ui.lua" 17 | times = 1 18 | 19 | [[patches]] 20 | [patches.pattern] 21 | match_indent = false 22 | pattern = ''' 23 | function UIElement:release(other) 24 | ''' 25 | payload = ''' 26 | if self.config.release_func then 27 | self.config.release_func(other) 28 | end 29 | ''' 30 | position = "after" 31 | target = "engine/ui.lua" 32 | times = 1 33 | 34 | [[patches]] 35 | [patches.pattern] 36 | match_indent = false 37 | pattern = ''' 38 | --Draw the outline of the object 39 | ''' 40 | payload = ''' 41 | if self.config.pulse_border then 42 | self.border_pulse_timer = self.border_pulse_timer or G.TIMERS.REAL 43 | local lw = 2*math.max(0, 0.5*math.cos(6*(G.TIMERS.REAL - self.border_pulse_timer)) + 0.5) 44 | prep_draw(self, 1) 45 | love.graphics.scale((1)/(G.TILESIZE)) 46 | love.graphics.setLineWidth(lw + 1) 47 | love.graphics.setColor(adjust_alpha(G.C.BLACK, 0.2*lw, true)) 48 | self:draw_pixellated_rect('fill', parallax_dist) 49 | love.graphics.setColor(self.config.colour[4] > 0 and mix_colours(G.C.WHITE, self.config.colour, 0.8) or G.C.WHITE) 50 | self:draw_pixellated_rect('line', parallax_dist) 51 | love.graphics.pop() 52 | end 53 | ''' 54 | position = "before" 55 | target = "engine/ui.lua" 56 | times = 1 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sticky Fingers 2 | 3 | A mod that brings "touch" controls to Balatro on PC/Mac, making it a bit more fun to play on desktop and improving the experience on touch-based PCs like the Steam Deck! The default mouse controls are kept intact. 4 | 5 | ![](https://github.com/eramdam/sticky-fingers/blob/meta/balatro-touch-mode-0001.png?raw=true) 6 | 7 | Wiimote cursor from [https://primm.gay/extras/other/cursors/](https://primm.gay/extras/other/cursors/) 8 | 9 | # Compatibility 10 | 11 | This mod has been tested against **Balatro 1.0.1o** (current patch as of February 26, 2025) on PC/Mac using code from the same patch on iOS. It has also been reported to work great on Steam Deck. 12 | 13 | As of 1.4.0, this mod should be compatible with the following mods: 14 | 15 | - [Cryptid](https://github.com/MathIsFun0/Cryptid/)/[Talisman](https://github.com/MathIsFun0/Talisman) 16 | - [Pokermon](https://github.com/InertSteak/Pokermon) 17 | - [Prism](https://github.com/blazingulag/Prism/) 18 | - [Reverie](https://github.com/jumbocarrot0/reverie) 19 | - [Bunco](https://github.com/jumbocarrot0/Bunco) (partially) 20 | 21 | Please let me know of mods that add consumable cards that may need additional support. 22 | 23 | # Installation 24 | 25 | 0. This mod is made for [**Steammodded 1.x**](https://github.com/Steamodded/smods) and [lovely](https://github.com/ethangreen-dev/lovely-injector) so you will need to install them first [as explained here](https://github.com/Steamodded/smods/wiki). **Disclaimer:** You will want to use the newest [tagged release of Steammodded](https://github.com/Steamodded/smods/releases) as the version on `main` might be broken/unstable. 26 | 1. Download the newest version of the mod from the [Releases tab](https://github.com/eramdam/balatro-mods/releases) 27 | 2. Extract the .zip file and move the `sticky-fingers` folder into your Mods file 28 | 3. Launch Balatro and enable the mod in the `Mods` panel 29 | 4. Enjoy dragging Jokers, Booster packs and consumables around! 30 | 31 | ## Disclaimer 32 | 33 | This mod was made from code belonging to LocalThunk and Playstack Games. All I wish for this mod is for a future patch of Balatro to make it obsolete :) 34 | -------------------------------------------------------------------------------- /lovely/ui_definitions.lua: -------------------------------------------------------------------------------- 1 | function drag_target(args) 2 | args = args or {} 3 | args.text = args.text or { 'BUY' } 4 | args.colour = copy_table(args.colour or G.C.UI.TRANSPARENT_DARK) 5 | args.cover = args.cover or nil 6 | args.emboss = args.emboss or nil 7 | args.active_check = args.active_check or (function(other) return true end) 8 | args.release_func = args.release_func or (function(other) G.DEBUG_VALUE = 'WORKIN' end) 9 | args.text_colour = copy_table(G.C.WHITE) 10 | args.uibox_config = { 11 | align = args.align or 'tli', 12 | offset = args.offset or { x = 0, y = 0 }, 13 | major = args.cover or args.major or nil, 14 | } 15 | 16 | local drag_area_width = (args.T and args.T.w or args.cover and args.cover.T.w or 0.001) + (args.cover_padding or 0) 17 | 18 | local text_rows = {} 19 | for k, v in ipairs(args.text) do 20 | text_rows[#text_rows + 1] = { n = G.UIT.R, config = { align = "cm", padding = 0.05, maxw = drag_area_width - 0.1 }, nodes = { { n = G.UIT.O, config = { object = DynaText({ scale = args.scale, string = v, maxw = args.maxw or (drag_area_width - 0.1), colours = { args.text_colour }, float = true, shadow = true, silent = not args.noisy, 0.7, pop_in = 0, pop_in_rate = 6, rotate = args.rotate or nil }) } } } } 21 | end 22 | 23 | args.DT = UIBox { 24 | T = { 0, 0, 0, 0 }, 25 | definition = 26 | { n = G.UIT.ROOT, config = { align = 'cm', args = args, can_collide = true, hover = true, release_func = args.release_func, func = 'check_drag_target_active', minw = drag_area_width, minh = (args.cover and args.cover.T.h or 0.001) + (args.cover_padding or 0), padding = 0.03, r = 0.1, emboss = args.emboss, colour = G.C.CLEAR }, nodes = text_rows }, 27 | config = args.uibox_config 28 | } 29 | args.DT.attention_text = true 30 | 31 | if G.OVERLAY_TUTORIAL and G.OVERLAY_TUTORIAL.highlights then 32 | G.OVERLAY_TUTORIAL.highlights[#G.OVERLAY_TUTORIAL.highlights + 1] = args.DT 33 | end 34 | 35 | G.E_MANAGER:add_event(Event({ 36 | trigger = 'after', 37 | delay = 0, 38 | blockable = false, 39 | blocking = false, 40 | func = function() 41 | if not G.CONTROLLER.dragging.target and args.DT then 42 | if G.OVERLAY_TUTORIAL and G.OVERLAY_TUTORIAL.highlights then 43 | for k, v in ipairs(G.OVERLAY_TUTORIAL.highlights) do 44 | if args.DT == v then 45 | table.remove(G.OVERLAY_TUTORIAL.highlights, k) 46 | break 47 | end 48 | end 49 | end 50 | args.DT:remove() 51 | return true 52 | end 53 | end 54 | })) 55 | end 56 | -------------------------------------------------------------------------------- /lovely/controller.toml: -------------------------------------------------------------------------------- 1 | [manifest] 2 | dump_lua = true 3 | priority = 0 4 | version = "1.4.0" 5 | 6 | [[patches]] 7 | [patches.pattern] 8 | match_indent = false 9 | pattern = ''' 10 | self.collision_list = {} --A list of all node that the cursor currently collides with 11 | ''' 12 | payload = ''' 13 | --Touch controller 14 | self.touch_control = {s_tap = {target = nil,handled = true}, l_press = {target = nil, handled = true}} 15 | ''' 16 | position = "after" 17 | target = "engine/controller.lua" 18 | times = 1 19 | 20 | [[patches]] 21 | [patches.pattern] 22 | match_indent = false 23 | pattern = ''' 24 | self.cursor_hover.time = G.TIMERS.TOTAL 25 | ''' 26 | payload = ''' 27 | self.cursor_hover.uptime = G.TIMERS.UPTIME 28 | ''' 29 | position = "after" 30 | target = "engine/controller.lua" 31 | times = 1 32 | 33 | [[patches]] 34 | [patches.pattern] 35 | match_indent = false 36 | pattern = ''' 37 | self:L_cursor_press(self.L_cursor_queue.x, self.L_cursor_queue.y) 38 | ''' 39 | payload = ''' 40 | self.cursor_down.uptime = G.TIMERS.UPTIME 41 | ''' 42 | position = "after" 43 | target = "engine/controller.lua" 44 | times = 1 45 | 46 | [[patches]] 47 | [patches.pattern] 48 | match_indent = false 49 | pattern = ''' 50 | self.cursor_up.time = G.TIMERS.TOTAL 51 | ''' 52 | payload = ''' 53 | self.cursor_up.uptime = G.TIMERS.UPTIME 54 | ''' 55 | position = "after" 56 | target = "engine/controller.lua" 57 | times = 1 58 | 59 | [[patches]] 60 | [patches.pattern] 61 | match_indent = false 62 | pattern = ''' 63 | if not self.cursor_down.handled then 64 | ''' 65 | payload = ''' 66 | self.cursor_down.distance = 0 67 | self.cursor_down.duration = 0 68 | ''' 69 | position = "after" 70 | target = "engine/controller.lua" 71 | times = 1 72 | 73 | [[patches]] 74 | [patches.pattern] 75 | match_indent = false 76 | pattern = ''' 77 | --The object being dragged 78 | ''' 79 | payload = ''' 80 | --The object being dragged 81 | if not self.dragging.handled and self.cursor_down.duration and (self.cursor_down.duration > 0.1) then 82 | create_drag_target_from_card(self.dragging.target) 83 | self.dragging.handled = true 84 | end 85 | ''' 86 | position = "before" 87 | target = "engine/controller.lua" 88 | times = 1 89 | 90 | [[patches]] 91 | [patches.pattern] 92 | match_indent = false 93 | pattern = ''' 94 | if self.hovering.target and self.hovering.target == self.dragging.target and not self.HID.touch then 95 | self.hovering.target:stop_hover() 96 | end 97 | ''' 98 | payload = ''' 99 | if self.is_cursor_down then 100 | self.cursor_down.distance = math.max(Vector_Dist(self.cursor_down.T, self.cursor_hover.T), self.cursor_down.distance or 0) 101 | self.cursor_down.duration = G.TIMERS.UPTIME - self.cursor_down.uptime 102 | if self.cursor_up.target then 103 | self.cursor_up.target = nil 104 | end 105 | end 106 | if not self.is_cursor_down then 107 | if self.cursor_down.target then 108 | self.cursor_down.target = nil 109 | self.cursor_down.distance = nil 110 | self.cursor_down.duration = nil 111 | end 112 | end 113 | ''' 114 | position = "after" 115 | target = "engine/controller.lua" 116 | times = 1 117 | 118 | [[patches]] 119 | [patches.pattern] 120 | match_indent = false 121 | pattern = ''' 122 | --if not, was the Cursor dragging some other thing? 123 | elseif self.dragging.prev_target and self.cursor_up.target and self.cursor_up.target.states.release_on.can then 124 | self.released_on.target = self.cursor_up.target 125 | self.released_on.handled = false 126 | end 127 | end 128 | ''' 129 | payload = ''' 130 | 131 | if self.cursor_down.distance < G.MIN_CLICK_DIST and self.cursor_down.duration < 0.2 then 132 | if self.cursor_down.target.states.click.can and not self.touch_control.l_press.target then 133 | self.touch_control.s_tap.target = self.cursor_down.target 134 | self.touch_control.s_tap.time = self.cursor_up.time - self.cursor_down.time 135 | self.touch_control.s_tap.handled = false 136 | end 137 | --if not, was the Cursor dragging some other thing? 138 | elseif self.dragging.prev_target then 139 | local releasable = nil 140 | for _, v in ipairs(self.collision_list) do 141 | if v.states.hover.can and (not v.states.drag.is) and (v ~= self.dragging.prev_target) then 142 | releasable = v 143 | break 144 | end 145 | end 146 | if releasable and releasable.states.release_on.can then 147 | self.released_on.target = releasable 148 | self.released_on.handled = false 149 | end 150 | end 151 | ''' 152 | position = "after" 153 | target = "engine/controller.lua" 154 | times = 1 155 | -------------------------------------------------------------------------------- /lovely/misc_functions.lua: -------------------------------------------------------------------------------- 1 | debug_current_card = {} 2 | function create_drag_target_from_card(_card) 3 | if _card and G.STAGE == G.STAGES.RUN then 4 | debug_current_card = _card 5 | 6 | G.DRAG_TARGETS = G.DRAG_TARGETS or { 7 | S_buy = Moveable { T = { x = G.jokers.T.x, y = G.jokers.T.y - 0.1, w = G.consumeables.T.x + G.consumeables.T.w - G.jokers.T.x, h = G.jokers.T.h + 0.6 } }, 8 | S_buy_and_use = Moveable { T = { x = G.deck.T.x + 0.2, y = G.deck.T.y - 5.1, w = G.deck.T.w - 0.1, h = 4.5 } }, 9 | C_sell = Moveable { T = { x = G.jokers.T.x, y = G.jokers.T.y - 0.2, w = G.jokers.T.w, h = G.jokers.T.h + 0.6 } }, 10 | J_sell = Moveable { T = { x = G.consumeables.T.x + 0.3, y = G.consumeables.T.y - 0.2, w = G.consumeables.T.w - 0.3, h = G.consumeables.T.h + 0.6 } }, 11 | J_sell_vanilla = Moveable { T = { x = G.consumeables.T.x + 0.3, y = G.consumeables.T.y - 0.2, w = G.consumeables.T.w - 0.3, h = G.consumeables.T.h + 0.6 } }, 12 | C_use = Moveable { T = { x = G.deck.T.x + 0.2, y = G.deck.T.y - 5.1, w = G.deck.T.w - 0.1, h = 4.5 } }, 13 | P_select = Moveable { T = { x = G.play.T.x, y = G.play.T.y - 2, w = G.play.T.w + 2, h = G.play.T.h + 1 } }, 14 | -- for Cryptid code cards and Pokermon item/energy cards (middle center) 15 | P_save = Moveable { T = { x = G.play.T.x, y = G.play.T.y - 2, w = G.play.T.w, h = G.play.T.h + 1 } }, 16 | -- Prism's "double cards" have a "Switch" button 17 | P_switch = Moveable { T = { x = G.deck.T.x + 0.2, y = G.deck.T.y - 5.1, w = G.deck.T.w - 0.1, h = 4.5 } }, 18 | } 19 | 20 | if DTM.config.vanilla_joker_sell == false then 21 | G.DRAG_TARGETS.J_sell = Moveable { T = { x = G.deck.T.x + 0.2, y = G.deck.T.y - 5.1, w = G.deck.T.w - 0.1, h = 4.5 } } 22 | else 23 | G.DRAG_TARGETS.J_sell = G.DRAG_TARGETS.J_sell_vanilla 24 | end 25 | 26 | if _card.area then 27 | -- is the card in one of the shop areas? 28 | if _card.area == G.shop_jokers or _card.area == G.shop_vouchers or 29 | _card.area == G.shop_booster then 30 | local buy_loc = copy_table(localize((_card.ability.set == 'Voucher' and 'ml_redeem_target') or 31 | (_card.ability.set == 'Booster' and 'ml_open_target') or 'ml_buy_target')) 32 | buy_loc[#buy_loc + 1] = '$' .. _card.cost 33 | drag_target({ 34 | cover = G.DRAG_TARGETS.S_buy, 35 | colour = adjust_alpha(G.C.GREEN, 0.9), 36 | text = buy_loc, 37 | card = _card, 38 | active_check = (function(other) 39 | return sticky_can_buy(other) 40 | end), 41 | release_func = (function(other) 42 | if other.ability.set == 'Joker' and sticky_can_buy(other) then 43 | if G.OVERLAY_TUTORIAL and G.OVERLAY_TUTORIAL.button_listen == 'buy_from_shop' then 44 | G.FUNCS.tut_next() 45 | end 46 | G.FUNCS.buy_from_shop({ 47 | config = { 48 | ref_table = other, 49 | id = 'buy' 50 | } 51 | }) 52 | return 53 | elseif (other.ability.set == 'Voucher' or other.ability.set == 'Booster') and sticky_can_buy(other) then 54 | G.FUNCS.use_card({ config = { ref_table = other } }) 55 | elseif sticky_can_buy(other) then 56 | G.FUNCS.buy_from_shop({ 57 | config = { 58 | ref_table = other, 59 | id = 'buy' 60 | } 61 | }) 62 | end 63 | end) 64 | }) 65 | 66 | if sticky_can_buy_and_use(_card) then 67 | local buy_use_loc = copy_table(localize('ml_buy_and_use_target')) 68 | buy_use_loc[#buy_use_loc + 1] = '$' .. _card.cost 69 | drag_target({ 70 | cover = G.DRAG_TARGETS.S_buy_and_use, 71 | colour = adjust_alpha(G.C.ORANGE, 0.9), 72 | text = buy_use_loc, 73 | card = _card, 74 | active_check = (function(other) 75 | return sticky_can_buy_and_use(other) 76 | end), 77 | release_func = (function(other) 78 | if sticky_can_buy_and_use(other) then 79 | G.FUNCS.buy_from_shop({ 80 | config = { 81 | ref_table = other, 82 | id = 'buy_and_use' 83 | } 84 | }) 85 | return 86 | end 87 | end) 88 | }) 89 | end 90 | end 91 | 92 | -- is the card in a pack? 93 | if _card.area == G.pack_cards then 94 | local is_consumeable_card_in_crazy_reverie_pack = Reverie and SMODS.OPENED_BOOSTER.label == 'Pack' and 95 | _card.ability.consumeable and _card.area == G.pack_cards 96 | 97 | -- is the card a consumeable? 98 | if _card.ability.consumeable then 99 | local draw_use_drag_target = function() 100 | local is_planet = _card.ability.set == 'Planet' 101 | drag_target({ 102 | cover = (is_planet and G.DRAG_TARGETS.P_select) or G.DRAG_TARGETS.C_use, 103 | colour = adjust_alpha((is_planet and G.C.GREEN) or G.C.RED, 0.9), 104 | text = { localize('b_use') }, 105 | card = _card, 106 | active_check = (function(other) 107 | return other:can_use_consumeable() 108 | end), 109 | release_func = (function(other) 110 | if other:can_use_consumeable() then 111 | G.FUNCS.use_card({ config = { ref_table = other } }) 112 | end 113 | end) 114 | }) 115 | end 116 | 117 | local needs_areas = true 118 | -- Pokermon has a "Super Rod" voucher that lets the player save any consumeable from packs and not just Energy/Item cards. 119 | local pokermon_has_save_all = G.GAME.poke_save_all and not SMODS.OPENED_BOOSTER.label:find("Wish") 120 | 121 | -- Cryptid's "Code" cards inside packs. 122 | if Cryptid and _card.ability.consumeable and _card.ability.set == 'Code' then 123 | draw_use_drag_target() 124 | -- "Pull" drag target ("use" area is already covered above) 125 | drag_target({ 126 | cover = G.DRAG_TARGETS.P_save, 127 | colour = adjust_alpha(G.C.GREEN, 0.9), 128 | text = { localize('b_pull') }, 129 | card = _card, 130 | active_check = (function(other) 131 | return sticky_can_reserve_card(other) 132 | end), 133 | release_func = (function(other) 134 | if sticky_can_reserve_card(other) then 135 | G.FUNCS.reserve_card({ config = { ref_table = other } }) 136 | end 137 | end) 138 | }) 139 | needs_areas = false 140 | -- Pokermon Item/Energy cards inside packs. 141 | elseif pokermon and _card.ability.consumeable and (_card.ability.set == 'Energy' or _card.ability.set == 'Item' or pokermon_has_save_all) then 142 | if not is_consumeable_card_in_crazy_reverie_pack then 143 | if not pokermon_has_save_all then 144 | draw_use_drag_target() 145 | else 146 | drag_target({ 147 | cover = G.DRAG_TARGETS.C_use, 148 | colour = adjust_alpha((is_planet and G.C.GREEN) or G.C.RED, 0.9), 149 | text = { localize('b_use') }, 150 | card = _card, 151 | active_check = (function(other) 152 | return other:can_use_consumeable() 153 | end), 154 | release_func = (function(other) 155 | if other:can_use_consumeable() then 156 | G.FUNCS.use_card({ config = { ref_table = other } }) 157 | end 158 | end) 159 | }) 160 | end 161 | 162 | -- "Save" drag target ("use" target is already covered above) 163 | drag_target({ 164 | cover = G.DRAG_TARGETS.P_save, 165 | colour = adjust_alpha( 166 | pokermon_has_save_all and G.C.GREEN or G.ARGS.LOC_COLOURS.pink, 0.9), 167 | text = { localize('b_save') }, 168 | card = _card, 169 | active_check = function(other) 170 | return sticky_can_reserve_card(other) 171 | end, 172 | release_func = function(other) 173 | if sticky_can_reserve_card(other) then 174 | G.FUNCS.reserve_card({ config = { ref_table = other } }) 175 | end 176 | end, 177 | }) 178 | needs_areas = false 179 | end 180 | elseif _card.ability.set == 'Cine' then 181 | drag_target({ 182 | cover = G.DRAG_TARGETS.P_select, 183 | colour = adjust_alpha(G.C.GREEN, 0.9), 184 | text = { localize('b_select') }, 185 | card = _card, 186 | active_check = (function(other) 187 | return sticky_can_select_card(other) 188 | end), 189 | release_func = (function(other) 190 | if sticky_can_select_card(other) then 191 | G.FUNCS.use_card({ config = { ref_table = other } }) 192 | end 193 | end), 194 | }) 195 | needs_areas = false 196 | elseif _card.ability.set == 'Auxiliary' then 197 | drag_target({ 198 | cover = G.DRAG_TARGETS.P_select, 199 | colour = adjust_alpha(G.C.GREEN, 0.9), 200 | text = { localize('b_select') }, 201 | card = _card, 202 | active_check = function(other) 203 | return sticky_can_take_card(other) 204 | end, 205 | release_func = function(other) 206 | if sticky_can_take_card(other) then 207 | G.FUNCS.take_card({ config = { ref_table = other } }) 208 | end 209 | end, 210 | }) 211 | needs_areas = false 212 | end 213 | 214 | if is_consumeable_card_in_crazy_reverie_pack and needs_areas then 215 | drag_target({ 216 | cover = G.DRAG_TARGETS.P_select, 217 | colour = adjust_alpha(G.C.GREEN, 0.9), 218 | text = { localize('b_select') }, 219 | card = _card, 220 | active_check = (function(other) 221 | return sticky_can_select_crazy_card(other) 222 | end), 223 | release_func = (function(other) 224 | if sticky_can_select_crazy_card(other) then 225 | G.FUNCS.use_card({ config = { ref_table = other } }) 226 | end 227 | end) 228 | }) 229 | elseif needs_areas then 230 | draw_use_drag_target() 231 | end 232 | else 233 | -- Bunco: Blind 'cards' in packs 234 | if BUNCOMOD and _card.ability.blind_card then 235 | drag_target({ 236 | cover = G.DRAG_TARGETS.P_select, 237 | colour = adjust_alpha(G.C.GREEN, 0.9), 238 | text = { localize('b_select') }, 239 | card = _card, 240 | active_check = (function(other) 241 | return sticky_can_use_blind_card(other) 242 | end), 243 | release_func = (function(other) 244 | if sticky_can_use_blind_card(other) then 245 | G.FUNCS.use_blind_card({ config = { ref_table = other } }) 246 | end 247 | end) 248 | }) 249 | else 250 | drag_target({ 251 | cover = G.DRAG_TARGETS.P_select, 252 | colour = adjust_alpha(G.C.GREEN, 0.9), 253 | text = { localize('b_select') }, 254 | card = _card, 255 | active_check = (function(other) 256 | return sticky_can_select_card(other) 257 | end), 258 | release_func = (function(other) 259 | if sticky_can_select_card(other) then 260 | G.FUNCS.use_card({ config = { ref_table = other } }) 261 | end 262 | end) 263 | }) 264 | end 265 | end 266 | end 267 | 268 | local draw_consumeable_use_target = function() 269 | drag_target({ 270 | cover = G.DRAG_TARGETS.C_use, 271 | colour = adjust_alpha(G.C.RED, 0.9), 272 | text = { localize('b_use') }, 273 | card = _card, 274 | active_check = (function(other) 275 | -- huge hack for Cryptid Code cards: since their `can_use` method might check for highlighted consumeables, we need to temporarily add the card inside the highlighted table to satisfy the check while drawing the drag_target 276 | if Cryptid and _card.ability.set == 'Code' and _card.area and not is_element_in_table(_card, _card.area.highlighted) then 277 | _card.area.highlighted[#_card.area.highlighted + 1] = _card 278 | local can_use = other:can_use_consumeable() 279 | remove_highlighted_card_from_area(_card, _card.area) 280 | return can_use 281 | end 282 | return other:can_use_consumeable() 283 | end), 284 | release_func = (function(other) 285 | -- this is lazy but if we're here we _should_ be good anyway 286 | if Cryptid and other.ability.set == 'Code' then 287 | G.FUNCS.use_card({ config = { ref_table = other } }) 288 | elseif other:can_use_consumeable() then 289 | G.FUNCS.use_card({ config = { ref_table = other } }) 290 | if G.OVERLAY_TUTORIAL and G.OVERLAY_TUTORIAL.button_listen == 'use_card' then 291 | G.FUNCS.tut_next() 292 | end 293 | end 294 | end) 295 | }) 296 | end 297 | 298 | -- is the card in the jokers/consumeables area? 299 | if _card.area and (_card.area == G.jokers or _card.area == G.consumeables) then 300 | local sell_loc = copy_table(localize('ml_sell_target')) 301 | sell_loc[#sell_loc + 1] = '$' .. (_card.facing == 'back' and '?' or _card.sell_cost) 302 | drag_target({ 303 | cover = _card.area == G.consumeables and G.DRAG_TARGETS.C_sell or G.DRAG_TARGETS.J_sell, 304 | colour = adjust_alpha(G.C.GOLD, 0.9), 305 | text = sell_loc, 306 | card = _card, 307 | active_check = (function(other) 308 | return other:can_sell_card() 309 | end), 310 | release_func = (function(other) 311 | G.FUNCS.sell_card { config = { ref_table = other } } 312 | end) 313 | }) 314 | if _card.area == G.consumeables then 315 | draw_consumeable_use_target() 316 | end 317 | end 318 | 319 | -- 'Cine' (Reverie) cards inside their own area. 320 | if _card.area and _card.area == G.cine_quests and _card.ability.consumeable and 321 | _card.ability.set == 'Cine' then 322 | local sell_loc = copy_table(localize('ml_sell_target')) 323 | sell_loc[#sell_loc + 1] = '$' .. (_card.facing == 'back' and '?' or _card.sell_cost) 324 | -- "Sell" target. 325 | drag_target({ 326 | cover = G.DRAG_TARGETS.C_sell, 327 | colour = adjust_alpha(G.C.GOLD, 0.9), 328 | text = sell_loc, 329 | card = _card, 330 | active_check = (function(other) 331 | return other:can_sell_card() 332 | end), 333 | release_func = (function(other) 334 | G.FUNCS.sell_card { config = { ref_table = other } } 335 | end), 336 | }) 337 | -- "Use" target. 338 | drag_target({ 339 | cover = G.DRAG_TARGETS.J_sell_vanilla, 340 | colour = adjust_alpha(G.C.RED, 0.9), 341 | text = { localize('b_use') }, 342 | card = _card, 343 | active_check = (function(other) 344 | return other:can_use_consumeable() 345 | end), 346 | release_func = (function(other) 347 | G.FUNCS.use_card({ config = { ref_table = other } }) 348 | end), 349 | }) 350 | end 351 | 352 | -- Prism's double cards from hand. 353 | if _card.area == G.hand and G.PRISM and _card.ability.set == 'Enhanced' and 354 | _card.ability.name == 'm_prism_double' then 355 | -- "Switch" area. 356 | drag_target({ 357 | cover = G.DRAG_TARGETS.P_switch, 358 | colour = adjust_alpha(G.C.RED, 0.9), 359 | text = { localize('prism_switch') }, 360 | card = _card, 361 | active_check = function() 362 | return true 363 | end, 364 | release_func = function(other) 365 | G.FUNCS.switch_button({ 366 | config = { 367 | ref_table = other, 368 | } 369 | }) 370 | end, 371 | }) 372 | end 373 | 374 | -- Cryptid's "MERGE" code card allows to merge playing cards with consumeables 375 | if _card.area == G.hand and _card.ability.consumeable then 376 | draw_consumeable_use_target() 377 | end 378 | end 379 | end 380 | end 381 | 382 | function remove_highlighted_card_from_area(card, area) 383 | for i = #area.highlighted, 1, -1 do 384 | if area.highlighted[i] == card then 385 | table.remove(card.area.highlighted, i) 386 | break 387 | end 388 | end 389 | end 390 | 391 | function is_element_in_table(element, table) 392 | for _, value in ipairs(table) do 393 | if value == element then 394 | return true 395 | end 396 | end 397 | return false 398 | end 399 | 400 | sticky_can_use_blind_card = function(_card) 401 | local temp_config = { UIBox = { states = { visible = false } }, config = { ref_table = _card } } 402 | G.FUNCS.can_use_blind_card(temp_config) 403 | return temp_config.config.button ~= nil; 404 | end 405 | 406 | sticky_can_reserve_card = function(_card) 407 | local temp_config = { UIBox = { states = { visible = false } }, config = { ref_table = _card } } 408 | G.FUNCS.can_reserve_card(temp_config) 409 | return temp_config.config.button ~= nil; 410 | end 411 | 412 | sticky_can_select_card = function(_card) 413 | local temp_config = { UIBox = { states = { visible = false } }, config = { ref_table = _card } } 414 | G.FUNCS.can_select_card(temp_config) 415 | return temp_config.config.button ~= nil; 416 | end 417 | 418 | sticky_can_buy = function(_card) 419 | local temp_config = { UIBox = { states = { visible = false } }, config = { ref_table = _card } } 420 | G.FUNCS.can_buy(temp_config) 421 | return temp_config.config.button ~= nil; 422 | end 423 | 424 | sticky_can_buy_and_use = function(_card) 425 | local temp_config = { UIBox = { states = { visible = false } }, config = { ref_table = _card } } 426 | G.FUNCS.can_buy_and_use(temp_config) 427 | return temp_config.config.button ~= nil; 428 | end 429 | 430 | sticky_can_select_crazy_card = function(_card) 431 | local temp_config = { UIBox = { states = { visible = false } }, config = { ref_table = _card } } 432 | G.FUNCS.can_select_crazy_card(temp_config) 433 | return temp_config.config.button ~= nil; 434 | end 435 | 436 | sticky_can_take_card = function(_card) 437 | local temp_config = { UIBox = { states = { visible = false } }, config = { ref_table = _card } } 438 | G.FUNCS.can_take_card(temp_config) 439 | return temp_config.config.button ~= nil; 440 | end 441 | --------------------------------------------------------------------------------