From 8acf37a68f55b741ed9def8a48305267284f41f4 Mon Sep 17 00:00:00 2001 From: Eremel Date: Tue, 2 Jun 2026 00:07:54 +0100 Subject: [PATCH 1/4] Add `SMODS.RunSelectPage` and all necessary functionality --- assets/1x/locked_stake.png | Bin 0 -> 1253 bytes assets/2x/locked_stake.png | Bin 0 -> 1485 bytes localization/en-us.lua | 14 +- lovely/back.toml | 2 +- lovely/run_select.toml | 68 +++ lovely/stake.toml | 4 +- lovely/ui_elements.toml | 32 +- src/game_object.lua | 15 + src/game_objects/runselectpage.lua | 219 +++++++ src/overrides.lua | 6 +- src/utils.lua | 40 ++ src/utils/run_select.lua | 906 +++++++++++++++++++++++++++++ 12 files changed, 1299 insertions(+), 7 deletions(-) create mode 100644 assets/1x/locked_stake.png create mode 100644 assets/2x/locked_stake.png create mode 100644 lovely/run_select.toml create mode 100644 src/game_objects/runselectpage.lua create mode 100644 src/utils/run_select.lua diff --git a/assets/1x/locked_stake.png b/assets/1x/locked_stake.png new file mode 100644 index 0000000000000000000000000000000000000000..6fbbc55888d37ed75d4dbc3286cd199c7362ccdc GIT binary patch literal 1253 zcmVPx(pGibPR7i=PmQiS1RT#&AH%-&VHn%m&NVayeu4Y}?&|0vRZgZkOtcB6)K!#GB zD43V_WsFkx;CvXROkd`U3S%8(J_yxK!6{7H&_Wq9Ixwv3wY0KT5_*%Bn}uX)o9jb% z&hE`kv+5rX+?;#9?{~iMeBV7$*raq_CvH3>x3el1&ou|B0&`|5;aK0%6ar8d?&~bx*I8Uj%6BI|>UXSnRoIXTn2SqE zv!yv`R)LwpMcObNBU5x^wloLL#igVvY&RY0wc9abGmGSNYv%h$_Hll43V^qd8~|W= zWYpp0?D!Y}-%S1nfW=OEsrz}}cyZ6h&}+A2tgfz7U0rSd^utYtqW#j@?>B!0;Iq*a z0JOHYO1ps%qX6vh>XV!M{EI6bI@~20Rc0K z>U#hZ%P9c%9o!9oPgNaYw=fL*>WHT^5-1F(Zp@NcPD$|bu`>>6q@|nC7QX`skTwi_ zs%lyTbV0Elyfq#=nnHB8x7i$O3)4i89w4!t;=5mBL|VEzH-1_UD=>QW0NTQ|&Azj} zjgF=eJNymky8Z+T&`{$sA6KCSo2z-5J^>zz_LIRS$56Ci0)>Q~@RPwsqqkO8^M)6@U-oau@EYM&ez~!qKL{?1 z&Ecx`c!+27)KQQxON;9NsuUQ{=Ap`sE4N;#U}l$|Oft|rD5D!58Fid#Y0;G`w_cE2 zDSMd%|M|QsNv*C@09<>wZ=<%#WlQm`J9o<(i9)iTX{ACEene{X1(Utow-qC`rg$Tu zRq1R_j;G1$PjD1~Kkxk`=FNEO0itIoCMJlqbOXhYKxvN(S}WVRmDi^;^I*DZr}DK zQfstxnq~s1fgl}CAu_mZ;7Ss}Itt7zB`}i3`?En+l~5>z0}Al%uQ$Y_3B@Z)eDpsU zIr&uRx$)Bg#It$kG)+t^0B^l^5ZlB?U~!)cg}olmPfn3oPI2gP7x%OW033dGcd_v; zdJJ^{q0A+2S)ioTY1^ja**uBm6g@pVF_JnB{&hKDBBj2*zAUh8q*hs~TF9KZ@G}5C zJv*7#RsiVjd)eVjoL@OrgqGXD_3JY%{+*EjcYO8FC_O!U*t*qU5oFcB8Fv@^-Xzk1 P00000NkvXXu0mjfZ82Iu literal 0 HcmV?d00001 diff --git a/assets/2x/locked_stake.png b/assets/2x/locked_stake.png new file mode 100644 index 0000000000000000000000000000000000000000..7e7ebac2743a9e160115cd0ea92f372ebece9a9f GIT binary patch literal 1485 zcmV;;1v2`HP)Px)he!$@IP1J!j6G`OZ9ZzVAC{Cc!kTySp2JY+nw5!I2RF&gC!qwx5%#1fVLJ1R#^H z0idCwVOs5)HLbl}#Y#kNxbvOQeCro9)|=n!4S^fI-!doww4XW?ssCk-a{;*I+4R@n z=E*pr-%~xl=WTr9(MbKrhG=pM=M=^lbf@oIaf{Wc-@Atw`uZYu+TS=2fZ2=|02X>r z#&3@=KdGM>6EC z9owz)$9oT0W%qg&RmlXv(q#((c<}b66ZSttR3wLohXE)rFE0RmweK(hFSgp=P6WO` z{DoEj=H`u7xv8ni_j+reZ?(#IFMk9meL~*eWxW4GEz4zJ!m|$uCL3R(K_p%8d2s(x zt33C*I;;HH0~5~~S0eq!yPsS2Xa4E6%B$Ac-n}-Rmc0{_jSbO~IGU-Q4M6rnzb{Ei zcUEL|=UuWdZ{HcokmiLetnO5FH!6RvqUr4gqjYWkmss8P3nY z`DZ9?ZEZl&yYBkPcwd*yE3GZQ=fA#dpKtrt^~T%kiP*4VgYWrwELjY|Y(`~&!m}SJ zu7YAiv}BIDBqiNBku3}lS;yryTi2Vi$SU6U2NV#0Mw?cCTX!ze2B{1Uh*o+rr1$X4m;J= z%jHVr(fHy+RJ(PVbd9gKb)~tPNmW?&ZQeRjs}7M=IZSH5_Ti@iIMAN8j_Z0^@A^S= zRrYJABHNcsK#YhcemZs>fOGka6Xv#xWAc7mw!Q`w33T`P@6t&P^=7y5$k7g~-p!5O z_R^t*-)bti$x9xavx)nmMV;m=V?b6K{7fjOgw?9|A>^QV>y*5fNAJswL=r zN&P(Y)RSfaAw=;qq}UKm>fJ}syOA(WmZajPyllsxKX^wbCOcJWeDNV#Rbig-Wp#6a z;*Pm1t@n2CF=ctrs07^1$0a*i-E*7S6AFs^vNxL>0OCkAn>Sun8HjtQ!ZN1+TA?Jp z@o0Qkm-Sar$-zsbSEOTkW5rv|MR#t7#ZmR+#?z?M_~JuUe`=Ct+G8mMwDncce(wJ* zjVCu^R;KYGdg~4I0XW`e)_+0XH)vlsNN#2n9!Dpa8Oa`ZUw6#gmKnP>zW5N;pCIo$ zS&~6=cMYdYvTya0W~<&EPi%;e?PmXavI~HUS*B`ev|#pJdd~u->r}mSQ5=*s$HLa3 zu7cvJyt-C7?mHN*J2Q3H%Kn6BKM=z?TJkPYWbxvu_GZ%c=B<>-%Gt} ztvM-@eLZI1R(n}^V-iVehG^I6(*WdhIV)2VS$(^wI2%NEs&YvxPU_o2k(6p`Y7zkR z=b82N3{h8~e7oPRvdC&qxI6aGZQ3#|hP}6An^jiEBcCx&))*eYe~oW{Geo5zvg5p* zuS8nk?#YYli?5R%XRY=v_nNOxL@r3[\t ]*)tab_definition_function = G.UIDEF.run_setup_option,[\n\t ]*tab_definition_function_args = 'New Run' +''' +position = 'at' +line_prepend = "$indent" +payload = ''' +tab_definition_function = G.UIDEF.run_select_galdur, +tab_definition_function_args = 'New Run' + +''' + +# Game:start_run() +# Allow run_select deck_choice to work +[[patches]] +[patches.pattern] +target = 'game.lua' +pattern = "local selected_back = saveTable and saveTable.BACK.name or (args.challenge and args.challenge.deck and args.challenge.deck.type) or (self.GAME.viewed_back and self.GAME.viewed_back.name) or self.GAME.selected_back and self.GAME.selected_back.name or 'Red Deck'" +position = 'at' +match_indent = true +payload = ''' +local selected_back = saveTable and saveTable.BACK.name or (args.challenge and args.challenge.deck and args.challenge.deck.type) or (args.deck_choice and args.deck_choice.name) or (self.GAME.viewed_back and self.GAME.viewed_back.name) or self.GAME.selected_back and self.GAME.selected_back.name or 'Red Deck' +''' +# Allow run_select stake_choice to work +[[patches]] +[patches.pattern] +target = 'game.lua' +match_indent = true +position = 'at' +pattern = ''' +self.GAME.stake = args.stake or self.GAME.stake or 1 +''' +payload = ''' +self.GAME.stake = args.stake_choice or args.stake or self.GAME.stake or 1 +''' + +# Allows hovering of chips in stake chip tower +# Controller:update() +[[patches]] +[patches.pattern] +target = 'engine/controller.lua' +pattern = '''if self.hovering.target and self.hovering.target == self.dragging.target and not self.HID.touch then''' +position = 'before' +match_indent = true +payload = ''' +if self.hovering.prev_target and self.hovering.prev_target.role and self.hovering.prev_target.role.major and self.hovering.prev_target.role.major.params and self.hovering.prev_target.role.major.params.stake and self.hovering.target ~= self.hovering.prev_target then self.hovering.prev_target.role.major:stop_hover() end +if self.hovering.target and self.hovering.target.role and self.hovering.target.role.major and self.hovering.target.role.major.params and self.hovering.target.role.major.params.stake and self.hovering.target ~= self.hovering.prev_target then self.hovering.target.role.major:hover() end +''' + +# Pressing b restarts properly +[[patches]] +[patches.pattern] +target = 'engine/controller.lua' +pattern = '''G:start_run({})''' +position = 'at' +match_indent = true +payload = ''' +G.FUNCS.run_select_quick_start() +''' \ No newline at end of file diff --git a/lovely/stake.toml b/lovely/stake.toml index e3016925b..788a6e39a 100644 --- a/lovely/stake.toml +++ b/lovely/stake.toml @@ -238,7 +238,7 @@ payload = ''' local t, res = {}, {} if _stake_center then if _stake_center.loc_vars and type(_stake_center.loc_vars) == 'function' then - res = _stake_center:loc_vars() or {} + res = _stake_center:loc_vars({}) or {} end t.vars = res.vars or {} t.key = res.key or _stake_center.key @@ -272,7 +272,7 @@ localize{type = 'descriptions', key = _stake_center.key, set = _stake_center.set payload = ''' local t, res = {}, {} if _stake_center.loc_vars and type(_stake_center.loc_vars) == 'function' then - res = _stake_center:loc_vars() or {} + res = _stake_center:loc_vars({}) or {} end t.vars = res.vars or {} t.key = res.key or _stake_center.key diff --git a/lovely/ui_elements.toml b/lovely/ui_elements.toml index 88f2b38ff..53a9089e3 100644 --- a/lovely/ui_elements.toml +++ b/lovely/ui_elements.toml @@ -305,4 +305,34 @@ for _, v in ipairs(text_shaders) do if v then self:set_text_shader() end end """ -match_indent = true \ No newline at end of file +match_indent = true + + +# Slider adjustments +# Sliders now round their values +# side effect: sliders with a smaller range of options may "slide" in a blocky way +[[patches]] +[patches.pattern] +target = 'functions/button_callbacks.lua' +match_indent = true +position = 'after' +pattern = ''' +rt.ref_table[rt.ref_value] = math.min(rt.max,math.max(rt.min, rt.min + (rt.max - rt.min)*(G.CURSOR.T.x - e.parent.T.x - G.ROOM.T.x)/e.T.w)) +''' +payload = ''' + local modifier = 10^(rt.decimal_places or 0) + rt.ref_table[rt.ref_value] = math.floor(rt.ref_table[rt.ref_value] * modifier + 0.5)/modifier +''' + +# Customise background colour of sliders +[[patches]] +[patches.pattern] +target = 'functions/UI_definitions.lua' +match_indent = true +position = 'at' +pattern = ''' +{n=G.UIT.C, config={align = "cl", minw = args.w, r = 0.1,min_h = args.h,collideable = true, hover = true, colour = G.C.BLACK,emboss = 0.05,func = 'slider', refresh_movement = true}, nodes={ +''' +payload = ''' +{n=G.UIT.C, config={align = "cl", minw = args.w, r = 0.1,min_h = args.h,collideable = true, hover = true, colour = args.background_colour or G.C.BLACK,emboss = 0.05,func = 'slider', refresh_movement = true}, nodes={ +''' \ No newline at end of file diff --git a/src/game_object.lua b/src/game_object.lua index ca499b2bd..a122ccba8 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -842,6 +842,9 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. modifiers = function() G.GAME.modifiers.enable_eternals_in_shop = true end, + loc_vars = function(self, info_queue, card) + info_queue[#info_queue+1] = {set = 'Other', key = 'eternal'} + end, colour = G.C.BLACK, loc_txt = {} } @@ -881,6 +884,9 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. modifiers = function() G.GAME.modifiers.enable_perishables_in_shop = true end, + loc_vars = function(self, info_queue, card) + info_queue[#info_queue+1] = {set = 'Other', key = 'perishable', vars = {5, 5}} + end, colour = G.C.ORANGE, loc_txt = {}, } @@ -893,6 +899,9 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. modifiers = function() G.GAME.modifiers.enable_rentals_in_shop = true end, + loc_vars = function(self, info_queue, card) + info_queue[#info_queue+1] = {set = 'Other', key = 'rental', vars = {G.GAME.rental_rate or 1}} + end, colour = G.C.GOLD, shiny = true, loc_txt = {} @@ -4020,6 +4029,12 @@ SMODS.UndiscoveredCompat = { assert(load(NFS.read(SMODS.path..'src/card_draw.lua'), ('=[SMODS _ "src/card_draw.lua"]')))() + ------------------------------------------------------------------------------------------------- + ----- API IMPORT GameObject.RunSelectPage + ------------------------------------------------------------------------------------------------- + + assert(load(SMODS.NFS.read(SMODS.path..'src/game_objects/runselectpage.lua'), ('=[SMODS _ "src/game_objects/runselectpage.lua"]')))() + ------------------------------------------------------------------------------------------------- ----- INTERNAL API CODE GameObject._Loc_Post ------------------------------------------------------------------------------------------------- diff --git a/src/game_objects/runselectpage.lua b/src/game_objects/runselectpage.lua new file mode 100644 index 000000000..1f579d3fc --- /dev/null +++ b/src/game_objects/runselectpage.lua @@ -0,0 +1,219 @@ +SMODS.RunSelectPage = SMODS.GameObject:extend({ + obj_table = SMODS.RunSelect.Pages, + set = 'RunSelectPages', + obj_buffer = {}, + disable_mipmap = false, + required_params = { + 'key', + }, + amount = 10, + selection_limit = 1, + stack_size = 1, + register = function(self) + if self.registered then + sendWarnMessage(('Detected duplicate register call on object %s'):format(self.key), self.set) + return + end + self.name = self.name or self.key + SMODS.RunSelectPage.super.register(self) + end, + inject = function(self) + self.page = self.page or (#SMODS.RunSelect.Internals.pages + 1) + self.grid_size = self.grid_size or {2, 5} + self.amount = self.grid_size[1] * self.grid_size[2] + if self.generate_pool then + self.pool = self:generate_pool() + end + if not self.injected then + if self.quick_start_text then + table.insert(SMODS.RunSelect.Internals.quick_start_text_functions, self.quick_start_text) + end + table.insert(SMODS.RunSelect.Internals.pages, self.page, self.key) + for i = self.page + 1, #SMODS.RunSelect.Internals.pages do + SMODS.RunSelect.Pages[SMODS.RunSelect.Internals.pages[i]].page = SMODS.RunSelect.Pages[SMODS.RunSelect.Internals.pages[i]].page + 1 + end + self.injected = true + end + end, + process_loc_text = function() end, + handle_choice = function(self, choice, remove) + SMODS.RunSelect.Setup.choices[self.key] = SMODS.RunSelect.Setup.choices[self.key] or {} + if not remove then + if self.selection_limit > 1 then + if SMODS.table_size(SMODS.RunSelect.Setup.choices[self.key]) < self.selection_limit and not SMODS.RunSelect.Setup.choices[self.key][choice.config.center.key] then + SMODS.RunSelect.Setup.choices[self.key][choice.config.center.key] = true + else + if choice.juice_up then choice:juice_up() end + return + end + else + SMODS.RunSelect.Setup.choices[self.key] = choice.config.center.key + end + if SMODS.RunSelect.Internals.preview_area then SMODS.RunSelect.Functions.populate_preview_ui(self.key, choice.config.center.key, nil) end + else + if self.selection_limit == 1 then + SMODS.RunSelect.Setup.choices[self.key] = nil + else + SMODS.RunSelect.Setup.choices[self.key][choice.config.center.key] = nil + end + if SMODS.RunSelect.Internals.preview_area then SMODS.RunSelect.Functions.populate_preview_ui(self.key, choice, nil, true) end + end + end, + set_default = function(self, choice) + return self.selection_limit > 1 and (type(choice) == 'table' and choice or {choice}) or choice + end, + selected_text = function(self, selection) + if not selection then return end + return localize({set = self.type, key = selection, type = 'name_text'}) + end, + choose_random = function(self) + local selected = false + local options = {} + for i=1, #self.pool do + if self.pool[i].unlocked then + options[#options + 1] = self.pool[i].key + end + end + while not selected do + selected = pseudorandom_element(options, pseudoseed(os.time())) + if selected == SMODS.RunSelect.Setup.choices[self.key] and #options > 1 then selected = false end + end + play_sound('whoosh1', math.random()*0.2 + 0.99, 0.35) + self:handle_choice({config = {center = {key = selected}}}) + end +}) + +local function stick(card) + card.children.back.states.hover = card.states.hover + card.children.back.states.click = card.states.click + card.children.back.states.drag = card.states.drag + card.children.back.states.collide.can = false + card.children.back:set_role({major = card, role_type = 'Glued', draw_major = card}) +end + +SMODS.RunSelectPage({ + key = 'deck_choice', + type = 'Back', + area_type = 'deck', + automatic_preview = true, + random_select = true, + pool = G.P_CENTER_POOLS.Back, + stack_size = 10, + preview_size = 52, + quick_start_text = function() + return localize({type = 'name_text', set = 'Back', key = G.PROFILES[G.SETTINGS.profile].last_choices.deck_choice or 'b_red'}) + end, + set_default = function(self, choice) + return choice or 'b_red' + end, + create_selection_card = function(self, card_key, card_number, area) + local card = Card(area.T.x, area.T.y, G.CARD_W, G.CARD_H, nil, G.P_CENTERS[card_key] or G.P_CENTERS.b_red) + card.sprite_facing = 'back' + card.facing = 'back' + card.children.back:remove() + card.children.back = SMODS.create_sprite(card.T.x, card.T.y, card.T.w, card.T.h, G.ASSET_ATLAS[card.config.center.unlocked and card.config.center.atlas or 'centers'], card.config.center.unlocked and card.config.center.pos or {x = 4, y = 0}) + stick(card) + if card_number == self.stack_size then + card.sticker = get_deck_win_sticker(card.config.center) + end + return card + end +}) + +SMODS.RunSelectPage({ + key = 'stake_choice', + include_deck_preview = true, + include_stake_tower = true, + area_type = 'deck', + grid_size = {4, 8}, + random_select = true, + type = 'Stake', + generate_pool = function(self) + return G.P_CENTER_POOLS.Stake + end, + sprite_size = {w = 0.99, h = 0.99}, + quick_start_text = function() + return localize({type = 'name_text', set = 'Stake', key = G.P_CENTER_POOLS.Stake[G.PROFILES[G.SETTINGS.profile].last_choices.stake_choice or 1].key}) + end, + set_default = function(self, choice) + if not choice or choice > #G.P_CENTER_POOLS.Stake then return 1 else return self.is_stake_unlocked(G.P_CENTER_POOLS.Stake[choice]) and choice or 1 end + end, + handle_choice = function(self, choice, remove) + SMODS.RunSelect.Setup.choices[self.key] = choice + G.E_MANAGER:clear_queue('run_select') + SMODS.RunSelect.Functions.populate_stake_tower(choice) + end, + is_stake_unlocked = function(stake) + local unlocked = true + local save_data = G.PROFILES[G.SETTINGS.profile].deck_usage[SMODS.RunSelect.Setup.choices.deck_choice] and G.PROFILES[G.SETTINGS.profile].deck_usage[SMODS.RunSelect.Setup.choices.deck_choice].wins_by_key or {} + for _,v in ipairs(stake.applied_stakes or {}) do + if not G.PROFILES[G.SETTINGS.profile].all_unlocked and (not save_data or (save_data and not save_data[v])) then + unlocked = false + end + end + if save_data and save_data[stake.key] then + return true, true + end + return unlocked + end, + create_selection_card = function(self, stake_key, card_number, area) + local card = Card(area.T.x, area.T.y, self.sprite_size.w, self.sprite_size.h, nil, G.P_CENTERS.j_joker, {stake = stake_key}) + card.no_shadow = true + card.facing = 'back' + card.sprite_facing = 'back' + card.config.center = G.P_STAKES[stake_key] + + local unlocked, won = self.is_stake_unlocked(G.P_STAKES[stake_key]) + -- TODO: check this with new save strucutre + + + if not unlocked then + card.params.stake_chip_locked = true + end + card.children.back:remove() + card.children.back = SMODS.create_sprite(card.T.x, card.T.y, card.T.w, card.T.h, unlocked and G.P_STAKES[stake_key].atlas or 'locked_stake', unlocked and G.P_STAKES[stake_key].pos or {x=0, y=0}) + card.children.back.draw = function(_sprite) + _sprite.ARGS.send_to_shader = _sprite.ARGS.send_to_shader or {} + _sprite.ARGS.send_to_shader[1] = math.min(_sprite.VT.r*3, 1) + G.TIMERS.REAL/(18) + (_sprite.juice and _sprite.juice.r*20 or 0) + 1 + _sprite.ARGS.send_to_shader[2] = G.TIMERS.REAL + + if won or area == SMODS.RunSelect.Internals.stake_tower_holding then + Sprite.draw_shader(_sprite, 'dissolve') + if card.config.center.shiny then Sprite.draw_shader(_sprite, 'voucher', nil, _sprite.ARGS.send_to_shader) end + else + Sprite.draw_shader(_sprite, 'played') + G.BRUTE_OVERLAY = {0.4, 0.4, 0.4, 0.4} + if card.config.center.shiny then Sprite.draw_shader(_sprite, 'negative_shine', nil, _sprite.ARGS.send_to_shader) end + G.BRUTE_OVERLAY = nil + end + end + + stick(card) + return card + end, + choose_random = function(self) + local selected = false + local options = {} + for i=1, #self.pool do + local unlocked = self.is_stake_unlocked(G.P_CENTER_POOLS.Stake[i]) + -- TODO: check this with new save strucutre + + if unlocked then + options[#options + 1] = i + end + end + while not selected do + selected = pseudorandom_element(options, pseudoseed(os.time())) + if selected == SMODS.RunSelect.Setup.choices[self.key] and #options > 1 then selected = false end + end + play_sound('whoosh1', math.random()*0.2 + 0.99, 0.35) + self:handle_choice(selected) + end +}) + +SMODS.Atlas({ -- art by nekojoe + key = 'locked_stake', + path = 'locked_stake.png', + px = 29, + py = 29 +}) \ No newline at end of file diff --git a/src/overrides.lua b/src/overrides.lua index 50583a8ff..ba1ecdf5c 100644 --- a/src/overrides.lua +++ b/src/overrides.lua @@ -531,7 +531,7 @@ function SMODS.applied_stakes_UI(i, stake_desc_rows, num_added) local _stake_center = G.P_CENTER_POOLS.Stake[i] local t, res = {}, {} if _stake_center.loc_vars and type(_stake_center.loc_vars) == 'function' then - res = _stake_center:loc_vars() or {} + res = _stake_center:loc_vars({}) or {} end t.vars = res.vars or {} t.key = res.key or _stake_center.key @@ -2409,7 +2409,9 @@ end function Card:align_h_popup() local focused_ui = self.children.focused_ui and true or false local popup_direction = (self.children.buy_button or (self.area and self.area.config.view_deck) or (self.area and self.area.config.type == 'shop')) and 'cl' or - (self.T.y > G.CARD_H*0.8 and self.T.y < G.CARD_H*1.8) and ((self.T.x > G.ROOM.T.w*0.4) and "cl" or "cr") or + (self.params.run_select_stake_tower) and 'cl' or + (self.params.run_select_selection_choice and self.params.stake) and 'bm' or + ((self.T.y > G.CARD_H*0.8 and self.T.y < G.CARD_H*1.8) or self.params.run_select_selection_choice ) and ((self.T.x > G.ROOM.T.w*0.4) and "cl" or "cr") or (self.T.y < G.CARD_H*0.8) and 'bm' or 'tm' local sign = 1 diff --git a/src/utils.lua b/src/utils.lua index 1ac017f7c..b03f59573 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -4160,4 +4160,44 @@ function SMODS.add_to_deck(card, args) local area = args.area or G.jokers area:emplace(card) return card +end + +------------------------------------------------------------------------------------------------- +----- API IMPORT RunSelect +------------------------------------------------------------------------------------------------- + +assert(load(SMODS.NFS.read(SMODS.path..'src/utils/run_select.lua'), ('=[SMODS _ "src/utils/run_select.lua"]')))() + +function SMODS.table_size(t) + local size = 0 + for _,_ in pairs(t) do + size = size + 1 + end + return size +end + +function SMODS.split_string(_string, parts) + local length = string.len(_string) + local words = {} + for i in string.gmatch(_string, "%S+") do + table.insert(words, i) + end + local spaces = #words - 1 + local line_break = math.floor(length/(parts or 2)) + + local text_output = {} + for i=1, (parts or 2) do text_output[i] = '' end + + local line = 1 + for i, v in ipairs(words) do + if string.len(text_output[line]) > line_break or i > spaces+1 or (i == 2 and spaces == 1) then + line = line + 1 + end + text_output[line] = text_output[line] .. v .. " " + end + for i, v in ipairs(text_output) do + text_output[i] = string.sub(v, 1, string.len(v)-1) + end + + return text_output end \ No newline at end of file diff --git a/src/utils/run_select.lua b/src/utils/run_select.lua new file mode 100644 index 000000000..c03306215 --- /dev/null +++ b/src/utils/run_select.lua @@ -0,0 +1,906 @@ +SMODS.RunSelect = { + Pages = {}, + Internals = { + quick_start_text_functions = {}, + preview_texts = { + preview_text_1 = {}, + preview_text_2 = {}, + }, + previous_button_text = '', + next_button_text = '', + pages = {}, + current_page = 1, + select_areas = {}, + hover_index = 0, + }, + Setup = { + choices = { + seed = '', + }, + }, + Functions = {}, + Colours = { + play = HEX('00BE67'), + nav_button = HEX('3FC7EB'), + seed_input = copy_table(G.C.UI.TEXT_INACTIVE), + quick_start = G.C.ORANGE, + waiting = G.C.RED, + } +} + +-- Replaces the New Run tab in the Play menu +function G.UIDEF.run_select_galdur(type) + if not G.E_MANAGER.queues.run_select then + G.E_MANAGER.queues.run_select = {} + end + G.PROFILES[G.SETTINGS.profile].last_choices = G.PROFILES[G.SETTINGS.profile].last_choices or {} + if not G.SAVED_GAME then + G.SAVED_GAME = get_compressed(G.SETTINGS.profile..'/'..'save.jkr') + if G.SAVED_GAME ~= nil then G.SAVED_GAME = STR_UNPACK(G.SAVED_GAME) end + end + G.SETTINGS.current_setup = type + + for key, page in pairs(SMODS.RunSelect.Pages) do + SMODS.RunSelect.Setup.choices[key] = page:set_default(G.PROFILES[G.SETTINGS.profile].last_choices[key]) + end + SMODS.RunSelect.Setup.choices.seed = '' + + SMODS.RunSelect.Internals.current_page = 1 + SMODS.RunSelect.Functions.update_nav_bar() + + local t = + {n=G.UIT.ROOT, config={align = "cm", colour = G.C.CLEAR, minh = 6.6, minw = 6}, nodes={ + {n = G.UIT.C, nodes = { + {n=G.UIT.R, config = {align = "cm", minw = 3}, nodes ={ + {n = G.UIT.O, config = {id = 'run_select', object = UIBox{ + definition = SMODS.RunSelect.Functions.create_page(SMODS.RunSelect.Internals.pages[SMODS.RunSelect.Internals.current_page]), + config = {align = "cm", offset = {x=0,y=0}} + }}}, + }}, + SMODS.RunSelect.Functions.nav_bar() + }} + }} + + return t +end + +function SMODS.RunSelect.Functions.create_page(key) + local page_def = SMODS.RunSelect.Pages[key] + SMODS.RunSelect.Setup.choices[key] = page_def:set_default(G.PROFILES[G.SETTINGS.profile].last_choices[key]) + SMODS.RunSelect.Functions.build_selection_areas(key) + + local deck_preview, stake_tower, other_preview + if page_def.include_deck_preview then + SMODS.RunSelect.Functions.build_preview_areas('deck_choice') + deck_preview = SMODS.RunSelect.Functions.build_preview_ui('deck_choice', true) + SMODS.RunSelect.Functions.populate_preview_ui('deck_choice', SMODS.RunSelect.Setup.choices.deck_choice, true) + end + if page_def.include_stake_tower then + SMODS.RunSelect.Functions.build_stake_tower() + stake_tower = SMODS.RunSelect.Functions.build_stake_tower_ui() + SMODS.RunSelect.Functions.populate_stake_tower(SMODS.RunSelect.Setup.choices.stake_choice) + end + if page_def.automatic_preview then + SMODS.RunSelect.Functions.build_preview_areas(key) + other_preview = SMODS.RunSelect.Functions.build_preview_ui(key) + SMODS.RunSelect.Functions.populate_preview_ui(key, SMODS.RunSelect.Setup.choices[key], true) + end + + local previews = {n=G.UIT.C, nodes = { + {n=G.UIT.R, nodes = {other_preview, stake_tower, deck_preview}} + }} + + if page_def.random_select then + previews.nodes[#previews.nodes+1] = {n = G.UIT.R, config={align = 'cm'}, nodes = { + {n=G.UIT.C, config = {maxw = 2.5, minw = 2.5, minh = 0.6, r = 0.1, hover = true, ref_value = 1, button = 'random_type', page_key = page_def.key, colour = SMODS.RunSelect.Colours.nav_button, align = "cm", emboss = 0.1}, nodes = { + {n=G.UIT.R, config = {align = 'cm'}, nodes = {{n=G.UIT.T, config={text = localize('run_select_'..page_def.key .. '_random'), scale = 0.4, colour = G.C.WHITE}}}}, + {n=G.UIT.R, config = {align = 'cm'}, nodes = {{n=G.UIT.C, config={func = 'set_button_pip', focus_args = { button = 'triggerright', set_button_pip = true, offset = {x=-0.2, y = 0.3} }}}}} + }} + }} + end + + local page_ui = page_def.definition or + page_def.pool and function() + return {n=G.UIT.C, config = {padding = 0.1}, nodes ={ + SMODS.RunSelect.Functions.build_selection_ui(key), + SMODS.RunSelect.Functions.create_page_cycle(key, page_def.amount) + }} + end + or page_def.settings and function() + local settings = page_def:settings() + for _, node in ipairs(settings) do + settings[_] = {n=G.UIT.R, config = {align = 'cm', padding = 0.1}, nodes = {node}} + end + return {n=G.UIT.C, config = {padding = 0.1}, nodes = { + {n=G.UIT.R, config={align = "cm", minh = 0.5+G.CARD_H+G.CARD_H, minw = 8.7, colour = G.C.BLACK, padding = 0.15, r = 0.1, emboss = 0.05}, nodes = settings}, + {n=G.UIT.R, config={minh=0.8}} + -- Galdur.generate_areas_ui(key, page_def.display_rows and {math.min(page_def.display_rows[1], math.ceil(#page_def.pool/page_def.display_rows[2])), page_def.display_rows[2]}), + -- Galdur.create_page_cycle(key, page_def.amount) + }} + end + + return + {n=G.UIT.ROOT, config={align = "tm", minh = 3.8, colour = G.C.CLEAR}, nodes={ + page_ui(page_def), previews + }} +end + +function SMODS.RunSelect.Functions.nav_bar() + local quick_select_text = {} + for _, func in ipairs(SMODS.RunSelect.Internals.quick_start_text_functions) do + local text = func() + if text then table.insert(quick_select_text, text) end + end + + local t = {n=G.UIT.R, config = {align = "cm", minw = 3, offset = {x=0, y=-5}, padding = 0.15}, nodes = { + -- Previous Button + {n = G.UIT.C, config={align='cm'}, nodes = { + {n=G.UIT.C, config = {id = 'previous_selection', minw = 2, minh = 0.8, maxh = 0.8, r = 0.1, hover = true, ref_value = -1, colour = G.C.CLEAR, align = "cm"}, nodes = { + {n=G.UIT.R, config = {align = 'cm'}, nodes = { + {n=G.UIT.O, config={object = DynaText({string = {{ref_table = SMODS.RunSelect.Internals, ref_value = 'previous_button_text'}}, colours = {G.C.WHITE}, shadow = true, maxw = 1.8, pop_in_rate = 0, scale = 0.4, silent = true})}} + }}, + {n=G.UIT.R, config = {align = 'cm'}, nodes = { + {n=G.UIT.C, config={func = 'set_button_pip_prev', focus_args = { button = 'triggerleft', set_button_pip = true, offset = {x=-0.2, y = 0.3}}}} + }} + }} + }}, + -- Seed Input + {n=G.UIT.C, config={align = "cr", padding = 0.05}, nodes={ + {n=G.UIT.O, config={id = 'seed_input', align = "cm", object = UIBox{ + config = {offset = {x=0,y=0}, parent = e, type = 'cm'}, + definition = {n=G.UIT.ROOT, config={align = "cr", colour = G.C.CLEAR}, nodes={ + {n=G.UIT.R, config={align = "cm", minw = 0.1}, nodes={ + {n=G.UIT.C, config={maxw = 3.1}, nodes = { + create_text_input({id = 'run_select_seeded_input', w = 3, max_length = 2500, extended_corpus = true, ref_table = SMODS.RunSelect.Setup.choices, ref_value = 'seed', prompt_text = localize('k_enter_seed'), colour = SMODS.RunSelect.Colours.seed_input, hooked_colour = darken(SMODS.RunSelect.Colours.seed_input, 0.3)}) + }}, + {n=G.UIT.C, config={align = "cm", minw = 0.1}}, + UIBox_button({id = 'run_select_seeded_paste', label = localize('ml_paste_seed'), minw = 1, minh = 0.6, button = 'paste_seed', colour = SMODS.RunSelect.Colours.seed_input, scale = 0.3, col = true}) + }} + }}, + }}}, + }}, + -- Seed Toggle + {n=G.UIT.C, config={align = "cm", minw = 2, id = 'run_setup_seed'}, nodes={ + {n=G.UIT.R, config={align='cr'}, nodes = { + create_toggle{col = true, label = localize('run_setup_enable_seed'), label_scale = 0.25, w = 0, scale = 0.7, ref_table = SMODS.RunSelect.Setup.choices, ref_value = 'enable_seed', callback = SMODS.RunSelect.Functions.update_seed_input} + }} + }}, + -- Next Button + {n = G.UIT.C, config={align='cm'}, nodes = { + {n=G.UIT.C, config = {id = 'next_selection', minw = 2, minh = 0.8, maxh = 0.8, r = 0.1, hover = true, ref_value = 1, func = 'run_select_can_change_page', + button = 'run_select_change_page', colour = SMODS.RunSelect.Colours.nav_button, align = "cm", emboss = 0.1}, nodes = { + {n=G.UIT.R, config = {align = 'cm'}, nodes = { + {n=G.UIT.O, config={object = DynaText({string = {{ref_table = SMODS.RunSelect.Internals, ref_value = 'next_button_text'}}, colours = {G.C.WHITE}, shadow = true, maxw = 1.8, pop_in_rate = 0, scale = 0.4, silent = true})}} + }}, + {n=G.UIT.R, config = {align = 'cm'}, nodes = { + {n=G.UIT.C, config={func = 'set_button_pip', focus_args = { button = 'x', set_button_pip = true, offset = {x=-0.2, y = 0.3}}}} + }} + }} + }}, + -- Quick Start Button + {n = G.UIT.C, config={align='cm'}, nodes = { + {n=G.UIT.R, config = {maxw = 2, minw = 2, minh = 0.8, r = 0.1, hover = true, ref_value = 1, + button = 'run_select_quick_start', colour = SMODS.RunSelect.Colours.quick_start, align = "cm", emboss = 0.1, tooltip = {text = quick_select_text} }, nodes = { + {n = G.UIT.C, config = {align = 'cm'} , nodes = { + {n=G.UIT.R, config = {align = 'cm'}, nodes = { + {n=G.UIT.T, config={text = localize('run_select_quick_start'), scale = 0.4, colour = G.C.WHITE}} + }}, + {n=G.UIT.R, config = {align = 'cm'}, nodes = { + {n=G.UIT.C, config={func = 'set_button_pip', focus_args = {button = 'y', set_button_pip = true, offset = {x=-0.2, y = 0.3}}} + }}} + }} + }} + }} + }} + + return t +end + +function SMODS.RunSelect.Functions.update_nav_bar(ui) + local previous_active = SMODS.RunSelect.Internals.current_page > 1 + local prev_page_index = SMODS.RunSelect.Functions.get_page_key(-1) + local next_page_index = SMODS.RunSelect.Functions.get_page_key(1) + local final = SMODS.RunSelect.Internals.current_page == #SMODS.RunSelect.Internals.pages or next_page_index > #SMODS.RunSelect.Internals.pages + SMODS.RunSelect.Internals.previous_button_text = previous_active and '< ' .. localize('run_select_'..SMODS.RunSelect.Internals.pages[prev_page_index]) or '' + SMODS.RunSelect.Internals.next_button_text = final and localize('run_select_play') or (localize('run_select_'..SMODS.RunSelect.Internals.pages[next_page_index]) .. ' >') + if not ui then return end + + local prev_button = ui.UIBox:get_UIE_by_ID('previous_selection') + local next_button = ui.UIBox:get_UIE_by_ID('next_selection') + + prev_button.config.button = previous_active and 'run_select_change_page' or nil + prev_button.config.emboss = previous_active and 0.1 or 0 + prev_button.config.hover = previous_active and true or false + prev_button.config.colour = previous_active and SMODS.RunSelect.Colours.nav_button or G.C.CLEAR + + prev_button.children[1].children[1].config.object:remove() + prev_button.children[1].children[1].config.object = DynaText({string = {{ref_table = SMODS.RunSelect.Internals, ref_value = 'previous_button_text'}}, colours = {G.C.WHITE}, shadow = true, maxw = 1.8, pop_in_rate = 0, scale = 0.4, silent = true}) + next_button.children[1].children[1].config.object:remove() + next_button.children[1].children[1].config.object = DynaText({string = {{ref_table = SMODS.RunSelect.Internals, ref_value = 'next_button_text'}}, colours = {G.C.WHITE}, shadow = true, maxw = 1.8, pop_in_rate = 0, scale = 0.4, silent = true}) +end + +function SMODS.RunSelect.Functions.update_seed_input(value) + if value then + SMODS.RunSelect.Colours.seed_input = HEX('3FC7EB') + else + SMODS.RunSelect.Colours.seed_input = copy_table(G.C.UI.TEXT_INACTIVE) + end + local args = G.OVERLAY_MENU:get_UIE_by_ID('run_select_seeded_input').children[1].children[1].config.ref_table + args.colour = SMODS.RunSelect.Colours.seed_input + args.hooked_colour = darken(SMODS.RunSelect.Colours.seed_input, 0.3) + G.OVERLAY_MENU:get_UIE_by_ID('run_select_seeded_paste').config.colour = SMODS.RunSelect.Colours.seed_input + G.OVERLAY_MENU:get_UIE_by_ID('run_select_seeded_input_prompt').config.colour = lighten(copy_table(SMODS.RunSelect.Colours.seed_input),0.4) +end + +G.FUNCS.set_button_pip_prev = function(e) + if SMODS.RunSelect.Internals.current_page > 1 then + G.FUNCS.set_button_pip(e) + elseif e.children.button_pip then + e.children.button_pip:remove() + e.children.button_pip = nil + end +end + +G.FUNCS.run_select_can_change_page = function(e) + local page_def = SMODS.RunSelect.Pages[SMODS.RunSelect.Internals.pages[SMODS.RunSelect.Internals.current_page]] + if page_def.can_continue then + if not page_def:can_continue() then + e.config.button = nil + e.config.colour = SMODS.RunSelect.Colours.waiting + return + end + end + + local final = SMODS.RunSelect.Internals.current_page == #SMODS.RunSelect.Internals.pages or SMODS.RunSelect.Functions.get_page_key(1) > #SMODS.RunSelect.Internals.pages + + e.config.button = final and 'run_select_start_run' or 'run_select_change_page' + e.config.colour = final and SMODS.RunSelect.Colours.play or SMODS.RunSelect.Colours.nav_button +end + +G.FUNCS.run_select_change_page = function(e) + SMODS.RunSelect.Functions.change_page(e) +end + +G.FUNCS.run_select_start_run = function(e) + SMODS.RunSelect.Functions.start_run() +end + +G.FUNCS.run_select_quick_start = function(e) + SMODS.RunSelect.Functions.start_run(true) +end + +G.FUNCS.random_type = function(e) + local page_def = SMODS.RunSelect.Pages[e.config.page_key] + page_def:choose_random() +end + +function SMODS.RunSelect.Functions.start_run(_quick_start) + local run_args = {} + SMODS.RunSelect.Functions.clean_up() + + local access = _quick_start and G.PROFILES[G.SETTINGS.profile].last_choices or SMODS.RunSelect.Setup.choices + for k, v in pairs(access) do + run_args[k] = v + end + + if SMODS.RunSelect.Setup.choices.enable_seed then + run_args.seed = SMODS.RunSelect.Setup.choices.seed + else + run_args.seed = nil + end + + G.PROFILES[G.SETTINGS.profile].last_choices = copy_table(run_args) + G:save_settings() + + run_args.deck_choice = {name = G.P_CENTERS[run_args.deck_choice].name} + + G.FUNCS.start_run(nil, run_args) +end + +local start_run = Game.start_run +function Game:start_run(...) + start_run(self, ...) + for _, value in ipairs(SMODS.RunSelectPage.obj_buffer) do + local page = SMODS.RunSelect.Pages[value] + if (not page.optional or (page.optional and page:optional())) and page.start_run and type(page.start_run) == 'function' and (not page.pool or G.PROFILES[G.SETTINGS.profile].last_choices[value]) then + page:start_run(G.PROFILES[G.SETTINGS.profile].last_choices[value]) + end + end +end + +function SMODS.RunSelect.Functions.get_page_key(change) + local next_page = SMODS.RunSelect.Internals.current_page+change + local valid_page = false + while not valid_page and SMODS.RunSelect.Internals.pages[next_page] do + local page = SMODS.RunSelect.Pages[SMODS.RunSelect.Internals.pages[next_page]] + if page and page.optional then + valid_page = page:optional() + else + valid_page = true + end + if valid_page then return next_page end + next_page = next_page + change + end + return next_page +end + +function SMODS.RunSelect.Functions.change_page(ui) + SMODS.RunSelect.Functions.clean_up() + SMODS.RunSelect.Internals.current_page = SMODS.RunSelect.Functions.get_page_key(ui.config.ref_value) + SMODS.RunSelect.Functions.update_nav_bar(ui) + + local current_selector_page = ui.UIBox:get_UIE_by_ID('run_select') + if not current_selector_page then return end + current_selector_page.config.object:remove() + current_selector_page.config.object = UIBox{ + definition = SMODS.RunSelect.Functions.create_page(SMODS.RunSelect.Internals.pages[SMODS.RunSelect.Internals.current_page]), + config = {offset = {x=0,y=0}, parent = current_selector_page, type = 'cm'} + } + current_selector_page.UIBox:recalculate() +end + +function SMODS.RunSelect.Functions.build_selection_areas(key) + local page_def = SMODS.RunSelect.Pages[key] + local dim = page_def.sprite_size or {w = G.CARD_W, h = G.CARD_H} + + if next(SMODS.RunSelect.Internals.select_areas) then + for i, area in ipairs(SMODS.RunSelect.Internals.select_areas) do + for j=1, #G.I.CARDAREA do + if area == G.I.CARDAREA[j] then + table.remove(G.I.CARDAREA, j) + SMODS.RunSelect.Internals.select_areas[i] = nil + end + end + end + end + + SMODS.RunSelect.Internals.select_areas = {} + for i=1, page_def.amount do + SMODS.RunSelect.Internals.select_areas[i] = CardArea(G.ROOM.T.w, G.ROOM.T.h, dim.w, dim.h, + {card_limit = 5, type = page_def.area_type or 'title_2', highlight_limit = 0, deck_height = 0.75, thin_draw = 1, run_select = key}) + end +end + +function SMODS.RunSelect.Functions.build_selection_ui(key) + local page_def = SMODS.RunSelect.Pages[key] + local dim = {math.min(page_def.grid_size[1], math.ceil(#page_def.pool/page_def.grid_size[2])), page_def.grid_size[2]} + local ui_nodes = {} + local count = 1 + local pool_size = #page_def.pool + for row=1, dim[1] do + local row_container = {n=G.UIT.R, config = {minw = 5}, nodes = {}} + for col=1, dim[2] do + if count > pool_size then break end + local col_node = {n=G.UIT.O, config = {object = SMODS.RunSelect.Internals.select_areas[count], focus_args = {snap_to = true}}} + table.insert(row_container.nodes, col_node) + count = count + 1 + end + table.insert(ui_nodes, row_container) + end + + SMODS.RunSelect.Functions.populate_selection_ui(key, 1) + + return {n=G.UIT.R, config={align = "cm", minh = 0.45+G.CARD_H+G.CARD_H, colour = G.C.BLACK, padding = 0.15, r = 0.1, emboss = 0.05}, nodes=ui_nodes} +end + +function SMODS.RunSelect.Functions.populate_selection_ui(key, page) + local page_def = SMODS.RunSelect.Pages[key] + local areas = SMODS.RunSelect.Internals.select_areas + + local card_size = page_def.sprite_size or {w = G.CARD_W, h = G.CARD_H} + local count = 1 + (page - 1) * page_def.amount + + for i=1, (page_def.amount or 10) do + if count > #page_def.pool then return end + local stack_size = page_def.stack_size + for j=1, stack_size do + local card = page_def.create_selection_card and page_def:create_selection_card(page_def.pool[count].key, j, areas[i]) + or Card(areas[i].T.x, areas[i].T.y, card_size.w, card_size.h, nil, page_def.pool[count]) + card.params.run_select_selection_choice = {i, key} + + areas[i]:emplace(card) + end + count = count + 1 + end +end + +local cycler = function(args) + args = args or {} + args.left = args.left or '<' + args.right = args.right or '>' + args.colour = args.colour or SMODS.RunSelect.Colours.nav_button + args.button_colour = args.button_colour or G.C.WHITE + args.button = args.button or 'cycler_default' + args.switch_func = args.switch_func + args.hover = args.hover or true + args.object_table = args.object_table -- REQUIRED + args.page_size = args.page_size -- REQUIRED + args.page_label = args.page_label -- REQUIRED + args.label_colour = args.label_colour or G.C.WHITE + args.scale = args.scale or 0.5 + args.button_w = args.button_w or 3 + args.w = args.w or 8 + args.shadow = args.shadow or true + args.total_pages = math.ceil(SMODS.table_size(args.object_table)/args.page_size) + + local page_cycler_values = {} + + if not args.page_label then + page_cycler_values = {page = 1} + page_cycler_values.text = localize('k_page')..' '..page_cycler_values.page..'/'..args.total_pages + args.page_label = page_cycler_values + end + + local cycler = {n=G.UIT.R, config = {align = 'cm', minh = args.h or nil}, nodes = { + SMODS.table_size(args.object_table) > args.page_size and {n=G.UIT.C, config={pass_through = args, switch_func = args.switch_func, r = 0.1, colour = args.colour, minw = args.button_w * args.scale, align = 'tm', shadow = args.shadow, direction = -1, button = args.button, hover = args.hover, minh = 0.5}, nodes = { + {n=G.UIT.T, config = {text = args.left, scale = args.scale, colour = args.button_colour}} + }} or nil, + SMODS.table_size(args.object_table) > args.page_size and {n=G.UIT.C, config = {align = 'cm', minw = args.w * args.scale}, nodes = { + {n=G.UIT.O, config = {object = DynaText({string = {{ref_table = args.page_label, ref_value = 'text'}}, scale = args.scale, colours = {args.label_colour}, pop_in_rate = 0, silent = true})}} + }} or nil, + SMODS.table_size(args.object_table) > args.page_size and {n=G.UIT.C, config={pass_through = args, switch_func = args.switch_func, r = 0.1, colour = args.colour, minw = args.button_w * args.scale, align = 'tm', shadow = args.shadow, direction = 1, button = args.button, hover = args.hover, minh = 0.5}, nodes = { + {n=G.UIT.T, config = {text = args.right, scale = args.scale, colour = args.button_colour}} + }} or nil, + }} + + return cycler +end + +G.FUNCS.cycler_default = function(e) + local args = e.config.pass_through + local page_from = e.config.pass_through.page_label.page + local page_to = e.config.pass_through.page_label.page + e.config.direction + if page_to == 0 then page_to = args.total_pages + elseif page_to > args.total_pages then page_to = 1 end + e.config.pass_through.page_label.page = page_to + e.config.pass_through.page_label.text = localize('k_page')..' '..e.config.pass_through.page_label.page..'/'..args.total_pages + + if e.config.switch_func and type(e.config.switch_func) == 'function' then + e.config.switch_func({from = page_from, to = page_to}) + end +end + +function SMODS.RunSelect.Functions.create_page_cycle(key, count_per_page) + local page_def = SMODS.RunSelect.Pages[key] + local cycler_text = {} + local total_pages = math.ceil(#page_def.pool / count_per_page) + for i=1, total_pages do + table.insert(cycler_text, localize('k_page')..' '..i..' / '..total_pages) + end + + local switch_func = function(args) + SMODS.RunSelect.Functions.clean_up() + SMODS.RunSelect.Functions.populate_selection_ui(key, args.to) + end + + local cycle = cycler({ + object_table = page_def.pool, + page_size = count_per_page, + key = page_def.key..'_select_cycle', + switch_func = switch_func, + h = 0.8 + }) + + return {n=G.UIT.R, config = {align = 'cm'}, nodes = {cycle}} +end + +function SMODS.RunSelect.Functions.build_preview_areas(key) + local page_def = SMODS.RunSelect.Pages[key] + if SMODS.RunSelect.Internals.preview_area then + for i=1, #G.I.CARDAREA do + if SMODS.RunSelect.Internals.preview_area == G.I.CARDAREA[i] then + table.remove(G.I.CARDAREA, i) + SMODS.RunSelect.Internals.preview_area = nil + elseif SMODS.RunSelect.Internals.preview_area_holding == G.I.CARDAREA[i] then + table.remove(G.I.CARDAREA, i) + SMODS.RunSelect.Internals.preview_area_holding = nil + end + end + end + + SMODS.RunSelect.Internals.preview_area = CardArea(15.475, 0, G.CARD_W * 1.5, G.CARD_H, + {card_limit = page_def.preview_size or page_def.selection_limit, type = page_def.area_type or 'title_2', highlight_limit = 0, run_select_deck_preview = page_def.key == 'deck_choice'}) + SMODS.RunSelect.Internals.preview_area_holding = CardArea(15.475+2*G.CARD_W, -2*G.CARD_H, G.CARD_W, G.CARD_H, + {card_limit = page_def.preview_size or page_def.selection_limit, type = page_def.area_type or 'title_2', highlight_limit = 0}) +end + +function SMODS.RunSelect.Functions.update_preview_texts(page_def) + local preview_texts = SMODS.split_string(page_def.selected_text and page_def:selected_text(SMODS.RunSelect.Setup.choices[page_def.key]) or localize('run_select_nothing')) + for i, text in ipairs(preview_texts) do + SMODS.RunSelect.Internals.preview_texts['preview_text_'..i] = text + local dyna_text_container = G.OVERLAY_MENU:get_UIE_by_ID('preview_text_'..i) + dyna_text_container.config.object.scale = 0.7/math.max(1, string.len(text)/8) + end +end + +function SMODS.RunSelect.Functions.build_preview_ui(key, deck_preview) + local page_def = SMODS.RunSelect.Pages[key] + local preview_texts = SMODS.split_string(page_def.selected_text and page_def:selected_text(SMODS.RunSelect.Setup.choices[page_def.key]) or localize('run_select_nothing')) + + for i, text in ipairs(preview_texts) do + SMODS.RunSelect.Internals.preview_texts[(deck_preview and 'deck_' or '')..'preview_text_'..i] = text + end + + local preview_area_node = {n=G.UIT.R, config = {align = 'tm'}, nodes = { + {n=G.UIT.O, config = {object = SMODS.RunSelect.Internals.preview_area}} + }} + + return {n=G.UIT.C, config = {align = "tm", padding = 0.1}, nodes ={ + {n = G.UIT.R, config = {minh = 5.95, minw = 3, maxw = 3, colour = G.C.BLACK, r=0.1, align = "bm", padding = 0.15, emboss=0.05}, nodes = { + {n = G.UIT.R, config = {align = "cm", minh = 0.6, maxw = 2.8}, nodes = { + {n=G.UIT.O, config = {id = (deck_preview and 'deck_' or '')..'preview_text_1', object = DynaText({ + string = {{ref_table = SMODS.RunSelect.Internals.preview_texts, ref_value = (deck_preview and 'deck_' or '')..'preview_text_1'}}, + scale = 0.7/math.max(1, string.len(SMODS.RunSelect.Internals.preview_texts[(deck_preview and 'deck_' or '')..'preview_text_1'])/8), + colours = {G.C.GREY}, + pop_in_rate = 5, + silent = true + })}} + }}, + {n = G.UIT.R, config = {align = "cm", minh = 0.6, maxw = 2.8}, nodes = { + {n=G.UIT.O, config = {id = (deck_preview and 'deck_' or '')..'preview_text_2', object = DynaText({ + string = {{ref_table = SMODS.RunSelect.Internals.preview_texts, ref_value = (deck_preview and 'deck_' or '')..'preview_text_2'}}, + scale = 0.7/math.max(1, string.len(SMODS.RunSelect.Internals.preview_texts[(deck_preview and 'deck_' or '')..'preview_text_2'])/8), + colours = {G.C.GREY}, + pop_in_rate = 5, + silent = true + })}} + }}, + {n = G.UIT.R, config = {align = "cm", minh = 0.2}}, + preview_area_node, + {n = G.UIT.R, config = {minh = 0.8, align = 'bm'}, nodes = { + {n=G.UIT.O, config = {object = DynaText({ + string = {localize('run_select_selected')}, + colours = {G.C.GREY}, + scale = 0.75, + silent = true + })}} + }}, + }} + }} +end + +function SMODS.RunSelect.Functions.populate_preview_ui(key, to_add, silent, _remove) + local page_def = SMODS.RunSelect.Pages[key] + if page_def.selection_limit == 1 and not _remove then + remove_all(SMODS.RunSelect.Internals.preview_area.cards) + SMODS.RunSelect.Internals.preview_area.cards = {} + remove_all(SMODS.RunSelect.Internals.preview_area_holding.cards) + SMODS.RunSelect.Internals.preview_area_holding.cards = {} + end + + if _remove then + to_add:remove() + SMODS.RunSelect.Functions.update_preview_texts(page_def) + return + end + + if not SMODS.RunSelect.Setup.choices[page_def.key] or (type(SMODS.RunSelect.Setup.choices[page_def.key]) == 'table' and not next(SMODS.RunSelect.Setup.choices[page_def.key])) then + return + end + + local preview_area = SMODS.RunSelect.Internals.preview_area + local holding_area = SMODS.RunSelect.Internals.preview_area_holding + + local stack_size = page_def.preview_size or page_def.stack_size + local card_size = page_def.sprite_size or {w = G.CARD_W, h = G.CARD_H} + if type(to_add) == 'table' then + local temp = {} + for k, _ in pairs(to_add) do table.insert(temp, k) end + to_add = temp + stack_size = #to_add + end + for j=1, stack_size do + local card = page_def.create_selection_card and page_def:create_selection_card(type(to_add) == 'table' and to_add[j] or to_add, j, preview_area) + or Card(preview_area.T.x, preview_area.T.y, card_size.w, card_size.h, nil, G.P_CENTERS[type(to_add) == 'table' and to_add[j] or to_add]) + card.params.run_select_preview_card = page_def.key + + if silent then + preview_area:emplace(card) + else + holding_area:emplace(card) + G.E_MANAGER:add_event(Event({ + func = (function() + play_sound('card1', math.random()*0.2 + 0.99, 0.35) + if holding_area.cards and preview_area.cards then preview_area:draw_card_from(holding_area) end + return true + end) + }), 'run_select') + end + end + if not silent then SMODS.RunSelect.Functions.update_preview_texts(page_def) end +end + +function SMODS.RunSelect.Functions.build_stake_tower() + if SMODS.RunSelect.Internals.stake_tower then + for i=1, #G.I.CARDAREA do + if SMODS.RunSelect.Internals.stake_tower == G.I.CARDAREA[i] then + table.remove(G.I.CARDAREA, i) + SMODS.RunSelect.Internals.stake_tower = nil + elseif SMODS.RunSelect.Internals.stake_tower_holding == G.I.CARDAREA[i] then + table.remove(G.I.CARDAREA, i) + SMODS.RunSelect.Internals.stake_tower_holding = nil + end + end + end + + SMODS.RunSelect.Internals.stake_tower = CardArea(G.ROOM.T.w * 0.656, G.ROOM.T.y, 3.4*14/41, 3.4*14/41, + {type = 'deck', highlight_limit = 0, draw_layers = {'card'}, thin_draw = 1, run_select_stake_tower = true}) + SMODS.RunSelect.Internals.stake_tower_holding = CardArea(G.ROOM.T.w * 0.656, G.ROOM.T.y, 3.4*14/41, 3.4*14/41, + {type = 'deck', highlight_limit = 0, run_select_stake_tower = true}) +end + +function SMODS.RunSelect.Functions.build_stake_tower_ui() + return + {n=G.UIT.C, config = {align = "tm", padding = 0.1}, nodes ={ + {n = G.UIT.C, config = {minh = 5.95, minw = 1.5, maxw = 1.5, colour = G.C.BLACK, r=0.1, align = "bm", padding = 0.05, emboss=0.05}, nodes = { + {n=G.UIT.R, config={align = "cm"}, nodes={ + {n = G.UIT.O, config = {object = SMODS.RunSelect.Internals.stake_tower}} + }}, + {n=G.UIT.R, config={minh=0.2}} + }} + }} +end + +local function order_applied_stakes(stake_chain, stake) + local ordered_chain = {} + for i,v in ipairs(G.P_CENTER_POOLS.Stake) do + if stake_chain[i] and i~= stake then + ordered_chain[#ordered_chain+1] = v.key + end + end + ordered_chain[#ordered_chain+1] = G.P_CENTER_POOLS.Stake[stake].key + return ordered_chain +end + +function SMODS.RunSelect.Functions.populate_stake_tower(stake, silent) + remove_all(SMODS.RunSelect.Internals.stake_tower.cards) + SMODS.RunSelect.Internals.stake_tower.cards = {} + remove_all(SMODS.RunSelect.Internals.stake_tower_holding.cards) + SMODS.RunSelect.Internals.stake_tower_holding.cards = {} + local page_def = SMODS.RunSelect.Pages.stake_choice + stake = page_def:set_default(stake) + local applied_stakes = order_applied_stakes(SMODS.build_stake_chain(G.P_CENTER_POOLS.Stake[stake]), stake) + + for i, stake_key in ipairs(applied_stakes) do + local card = page_def:create_selection_card(stake_key, nil, SMODS.RunSelect.Internals.stake_tower_holding) + card.params.run_select_stake_tower = {G.P_STAKES[stake_key].order, stake_key} + card.params.hover = #applied_stakes - i + card.children.back.states.collide.can = true + SMODS.RunSelect.Internals.stake_tower_holding:emplace(card) + + if not silent then + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 0.02, + func = (function() + play_sound('chips2', math.random()*0.2 + 0.99, 0.35) + if SMODS.RunSelect.Internals.stake_tower.cards then SMODS.RunSelect.Internals.stake_tower:draw_card_from(SMODS.RunSelect.Internals.stake_tower_holding) end + return true + end) + }), 'run_select') + else + SMODS.RunSelect.Internals.stake_tower:emplace(card) + end + end +end + +local function order_stake_chain(stake_chain, _stake) + local ordered_chain = {} + for i,_ in ipairs(G.P_CENTER_POOLS.Stake) do + if stake_chain[i] and i~= _stake then + ordered_chain[#ordered_chain+1] = i + end + end + ordered_chain[#ordered_chain+1] = _stake + return ordered_chain +end + +function SMODS.RunSelect.Functions.clean_up() + for j = 1, #SMODS.RunSelect.Internals.select_areas do + if SMODS.RunSelect.Internals.select_areas[j].cards then + remove_all(SMODS.RunSelect.Internals.select_areas[j].cards) + SMODS.RunSelect.Internals.select_areas[j].cards = {} + end + end +end + +-- Function Hooks +local card_stop_hover = Card.stop_hover +function Card:stop_hover() + if self.params.stake then + SMODS.RunSelect.Internals.hover_index = 0 + end + card_stop_hover(self) +end + +function SMODS.RunSelect.Functions.grab_tooltips(set, key) + local info_queue = {} + local loc_target = G.localization.descriptions[set][key] + for _, lines in ipairs(loc_target.text_parsed) do + for _, part in ipairs(lines) do + if part.control.T then + info_queue[#info_queue+1] = G.P_CENTERS[part.control.T] or G.P_TAGS[part.control.T] or { + set = part.control.T_set or 'Other', + key = part.control.T, + vars = part.control.T_vars and parse_tooltip_vars(part.control.T_vars) or {} + } + end + end + end + return info_queue +end + +function SMODS.RunSelect.Functions.create_info_nodes(info_queue, c, row) + if (not c.config.center.unlocked and not c.params.stake) or c.params.stake_chip_locked then return {} end + + local info_queue = SMODS.RunSelect.Functions.grab_tooltips(c.config.center.set, c.config.center.key) + if c.config.center.loc_vars and type(c.config.center.loc_vars) == 'function' then + c.config.center:loc_vars(info_queue, c.config.center) + end + + local tooltips = {} + local total_tooltips = #info_queue + local column_count = total_tooltips == 0 and 0 or total_tooltips <= 3 and 1 or total_tooltips <= 8 and 2 or total_tooltips <= 18 and 3 or 4 + if column_count == 3 and c.T.x < G.ROOM.T.w*0.6 then column_count = 2 end + local nodes_per_col = math.ceil(total_tooltips/column_count) + + local function create_info_tooltip(tooltip_data) + local desc = generate_card_ui(tooltip_data, {main = {},info = {},type = {},name = 'done',badges = {}, from_detailed_tooltip = true}, nil, tooltip_data.set, nil) + return {n=row and G.UIT.C or G.UIT.R, config={align = 'cm'}, nodes={ + {n=G.UIT.R, config={align = "cm", colour = lighten(G.C.JOKER_GREY, 0.5), r = 0.1, padding = 0.05, emboss = 0.05}, nodes={ + info_tip_from_rows(desc.info[1], desc.info[1].name), + }} + }} + end + + for i = 0, column_count-1 do + local tooltip_group = {} + for j = 1, nodes_per_col do + local tooltip_data = info_queue[i*nodes_per_col+j] + if tooltip_data then + table.insert(tooltip_group, create_info_tooltip(tooltip_data)) + else break end + end + table.insert(tooltips, {n=row and G.UIT.R or G.UIT.C, (c.T.x > G.ROOM.T.w*0.4) and column_count-i or i+1, config = {align="cm", padding = 0.05}, nodes = tooltip_group}) + end + + return tooltips +end + + +local card_hover_ref = Card.hover +function Card:hover() + if (self.params.run_select_selection_choice or self.params.run_select_preview_card) and self.config.center.set == 'Back' and (not self.states.drag.is or G.CONTROLLER.HID.touch) and not self.no_ui and not G.debug_tooltip_toggle then + self:juice_up(0.05, 0.03) + play_sound('paper1', math.random()*0.2 + 0.99, 0.35) + + local back = Back(self.config.center) + local tooltips = SMODS.RunSelect.Functions.create_info_nodes({}, self) + + local badges = {n=G.UIT.C, config = {colour = G.C.CLEAR, align = 'cm'}, nodes = {}} + SMODS.create_mod_badges(self.config.center, badges.nodes) + if badges.nodes.mod_set then badges.nodes.mod_set = nil end + + self.config.h_popup = {n=G.UIT.C, config={align = 'cm', padding = 0.1}, nodes = { + next(tooltips) and {n=G.UIT.C, config={align='cm', padding=0.1}, nodes = tooltips} or nil + }} + + table.insert(self.config.h_popup.nodes, (self.T.x > G.ROOM.T.w*0.4) and 2 or 1, + {n=G.UIT.C, config={align='cm', padding = 0.1}, nodes = { + {n=G.UIT.C, config={align = "cm", minh = 1.5, r = 0.1, colour = G.C.L_BLACK, padding = 0.1, outline=1}, nodes={ + {n=G.UIT.R, config={align = "cm", r = 0.1, minw = 3, maxw = 4, minh = 0.4}, nodes={ + {n=G.UIT.O, config={object = DynaText({string = back:get_name(),maxw = 4, colours = {G.C.WHITE}, shadow = true, bump = true, scale = 0.5, pop_in = 0, silent = true})}} + }}, + {n=G.UIT.R, config={align = "cm", colour = G.C.WHITE, minh = 1.3, maxh = 3, minw = 3, maxw = 4, r = 0.1}, nodes={ + {n=G.UIT.O, config={object = UIBox{definition = back:generate_UI(), config = {offset = {x=0,y=0}}}}} + }}, + badges.nodes[1] and {n=G.UIT.R, config={align = "cm", r = 0.1, minw = 3, maxw = 4, minh = 0.4}, nodes={badges}}, + }} + }} + ) + + self.config.h_popup_config = self:align_h_popup() + Node.hover(self) + return + elseif (self.params.run_select_selection_choice or self.params.run_select_stake_tower) and self.params.stake and (not self.states.drag.is or G.CONTROLLER.HID.touch) and not self.no_ui and not G.debug_tooltip_toggle then + SMODS.RunSelect.Internals.hover_index = self.params.hover or 0 + self:juice_up(0.05, 0.03) + play_sound('paper1', math.random()*0.2 + 0.99, 0.35) + local stake = G.P_STAKES[self.config.center.key] + + local tooltips = SMODS.RunSelect.Functions.create_info_nodes({}, self, true) + + local badges = {n=G.UIT.R, config = {colour = G.C.CLEAR, align = 'cm', padding = 0.05}, nodes = {}} + SMODS.create_mod_badges(stake, badges.nodes) + if badges.nodes.mod_set then badges.nodes.mod_set = nil end + + local stake_desc + if self.params.stake_chip_locked then + local number_applied_stakes = #stake.applied_stakes + local string_output = localize('run_select_locked_stake_message') + for i,v in ipairs(stake.applied_stakes) do + string_output = string_output .. localize({type='name_text', set='Stake', key=v}) .. (i < number_applied_stakes and localize('run_select_locked_stake_and') or '') + end + local split = SMODS.split_string(string_output) + + stake_desc = {n=G.UIT.C, config={align = "cm", padding = 0.05, r = 0.1, colour = G.C.L_BLACK}, nodes={ + {n=G.UIT.R, config={align = "cm", padding = 0}, nodes={{n=G.UIT.T, config={text = localize('run_select_locked_stake'), scale = 0.35, colour = G.C.WHITE}}}}, + {n=G.UIT.R, config={align = "cm", padding = 0.03, colour = G.C.WHITE, r = 0.1, minh = 1, minw = 3}, nodes={ + {n=G.UIT.R, config={align='cm'}, nodes={{n=G.UIT.T, config={text = split[1], scale = 0.3, colour = G.C.UI.TEXT_DARK}}}}, + {n=G.UIT.R, config={align='cm'}, nodes={{n=G.UIT.T, config={text = split[2], scale = 0.3, colour = G.C.UI.TEXT_DARK}}}} + }} + }} + else + stake_desc = G.UIDEF.stake_description(self.config.center.order) + stake_desc.nodes[2].config.minw = 3 + end + + if badges.nodes[1] then table.insert(stake_desc.nodes, badges) end + + self.config.h_popup = {n = G.UIT.C, config={align='cm', colour = G.C.CLEAR}, nodes = { + {n=G.UIT.R, config={align='cm'}, nodes = { + {n=G.UIT.R, config={align = "cm", minh = 1.5, r = 0.1, colour = G.C.L_BLACK, padding = 0.05, outline=1}, nodes ={ + {n=G.UIT.C, config={align = "cm", padding = 0}, nodes={stake_desc}} + }}, + }}, + next(tooltips) and {n=G.UIT.R, config={align='cm', padding=0.1}, nodes = tooltips}, + }} + + self.config.h_popup_config = self:align_h_popup() + Node.hover(self) + return + elseif self.params.run_select_selection_choice or self.params.run_select_preview_card then + local page = SMODS.RunSelect.Pages[self.params.run_select_preview_card or self.params.run_select_selection_choice[2]] + if page.card_hover and type(page.card_hover) == 'function' then + return page:card_hover(self) + end + end + card_hover_ref(self) +end + +local card_click_ref = Card.click +function Card:click() + if self.params.stake and not self.params.stake_chip_locked and self.params.run_select_selection_choice then + SMODS.RunSelect.Pages.stake_choice:handle_choice(self.params.run_select_selection_choice[1]) + elseif self.params.run_select_selection_choice and self.config.center.unlocked ~= false and self.config.center.discovered ~= false then + local page = SMODS.RunSelect.Pages[self.params.run_select_selection_choice[2]] + if page.card_click and type(page.card_click) == 'function' then + return page:card_click(self) + else + page:handle_choice(self) + end + elseif self.params.run_select_preview_card then + if self.params.run_select_preview_card == 'deck_choice' then return end + local page = SMODS.RunSelect.Pages[self.params.run_select_preview_card] + if page.preview_click and type(page.preview_click) == 'function' then + return page:preview_click(self) + else + page:handle_choice(self, true) + end + else + card_click_ref(self) + end +end + +local card_area_align_ref = CardArea.align_cards +function CardArea:align_cards() + if self.config.run_select_stake_tower then -- align chips vertically in chip tower + local deck_height = 5.6/math.max(24,#self.cards) + for k, card in ipairs(self.cards) do + if not card.states.drag.is then + card.T.x = self.T.x + 0.5*(self.T.w - card.T.w) + card.T.y = self.T.y + deck_height - (#self.cards - k + (k <= SMODS.RunSelect.Internals.hover_index and 4 or 0))*deck_height + end + card.rank = 0 + end + elseif self.config.run_select_deck_preview then -- deck preview grows vertically + for k, card in ipairs(self.cards) do + if not card.states.drag.is then + card.T.x = self.T.x + 0.5*(self.T.w - card.T.w) + card.T.y = self.T.y + 0.5*(self.T.h - card.T.h) + self.shadow_parrallax.y*0.25/52*(#self.cards/2 - k) + end + end + else + card_area_align_ref(self) + end +end \ No newline at end of file From bbd459f70bc5712bdcec2ec67099ce5b4ff69394 Mon Sep 17 00:00:00 2001 From: Eremel Date: Tue, 2 Jun 2026 16:13:58 +0100 Subject: [PATCH 2/4] fix paste seed button --- src/utils/run_select.lua | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/utils/run_select.lua b/src/utils/run_select.lua index c03306215..ac3ca69e7 100644 --- a/src/utils/run_select.lua +++ b/src/utils/run_select.lua @@ -114,8 +114,6 @@ function SMODS.RunSelect.Functions.create_page(key) return {n=G.UIT.C, config = {padding = 0.1}, nodes = { {n=G.UIT.R, config={align = "cm", minh = 0.5+G.CARD_H+G.CARD_H, minw = 8.7, colour = G.C.BLACK, padding = 0.15, r = 0.1, emboss = 0.05}, nodes = settings}, {n=G.UIT.R, config={minh=0.8}} - -- Galdur.generate_areas_ui(key, page_def.display_rows and {math.min(page_def.display_rows[1], math.ceil(#page_def.pool/page_def.display_rows[2])), page_def.display_rows[2]}), - -- Galdur.create_page_cycle(key, page_def.amount) }} end @@ -154,7 +152,7 @@ function SMODS.RunSelect.Functions.nav_bar() create_text_input({id = 'run_select_seeded_input', w = 3, max_length = 2500, extended_corpus = true, ref_table = SMODS.RunSelect.Setup.choices, ref_value = 'seed', prompt_text = localize('k_enter_seed'), colour = SMODS.RunSelect.Colours.seed_input, hooked_colour = darken(SMODS.RunSelect.Colours.seed_input, 0.3)}) }}, {n=G.UIT.C, config={align = "cm", minw = 0.1}}, - UIBox_button({id = 'run_select_seeded_paste', label = localize('ml_paste_seed'), minw = 1, minh = 0.6, button = 'paste_seed', colour = SMODS.RunSelect.Colours.seed_input, scale = 0.3, col = true}) + UIBox_button({id = 'run_select_seeded_paste', label = localize('ml_paste_seed'), minw = 1, minh = 0.6, button = 'run_select_paste_seed', colour = SMODS.RunSelect.Colours.seed_input, scale = 0.3, col = true}) }} }}, }}}, @@ -274,6 +272,24 @@ G.FUNCS.random_type = function(e) page_def:choose_random() end +G.FUNCS.run_select_paste_seed = function(e) + G.CONTROLLER.text_input_hook = e.UIBox:get_UIE_by_ID('run_select_seeded_input').children[1].children[1] + G.CONTROLLER.text_input_id = 'run_select_seeded_input' + for i = 1, string.len(SMODS.RunSelect.Setup.choices.seed or '') do + G.FUNCS.text_input_key({key = 'right'}) + end + for i = 1, string.len(SMODS.RunSelect.Setup.choices.seed or '') do + G.FUNCS.text_input_key({key = 'backspace'}) + end + local clipboard = (G.F_LOCAL_CLIPBOARD and G.CLIPBOARD or love.system.getClipboardText()) or '' + for i = 1, #clipboard do + local c = clipboard:sub(i,i) + G.FUNCS.text_input_key({key = c}) + end + G.FUNCS.text_input_key({key = 'return'}) +end + + function SMODS.RunSelect.Functions.start_run(_quick_start) local run_args = {} SMODS.RunSelect.Functions.clean_up() From e57f07666d161d036cc509ad2ed1325c9a7024d8 Mon Sep 17 00:00:00 2001 From: Eremel Date: Wed, 3 Jun 2026 22:47:20 +0100 Subject: [PATCH 3/4] fex fixes: cardarea positioning, random button, `silent` parameter added --- src/game_objects/runselectpage.lua | 25 +++++++++++++++++-------- src/utils/run_select.lua | 22 +++++++++------------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/game_objects/runselectpage.lua b/src/game_objects/runselectpage.lua index 1f579d3fc..3fdb09083 100644 --- a/src/game_objects/runselectpage.lua +++ b/src/game_objects/runselectpage.lua @@ -9,6 +9,7 @@ SMODS.RunSelectPage = SMODS.GameObject:extend({ amount = 10, selection_limit = 1, stack_size = 1, + silent = false, register = function(self) if self.registered then sendWarnMessage(('Detected duplicate register call on object %s'):format(self.key), self.set) @@ -49,14 +50,14 @@ SMODS.RunSelectPage = SMODS.GameObject:extend({ else SMODS.RunSelect.Setup.choices[self.key] = choice.config.center.key end - if SMODS.RunSelect.Internals.preview_area then SMODS.RunSelect.Functions.populate_preview_ui(self.key, choice.config.center.key, nil) end + if SMODS.RunSelect.Internals.preview_area then SMODS.RunSelect.Functions.populate_preview_ui(self.key, choice.config.center.key, self.silent) end else if self.selection_limit == 1 then SMODS.RunSelect.Setup.choices[self.key] = nil else SMODS.RunSelect.Setup.choices[self.key][choice.config.center.key] = nil end - if SMODS.RunSelect.Internals.preview_area then SMODS.RunSelect.Functions.populate_preview_ui(self.key, choice, nil, true) end + if SMODS.RunSelect.Internals.preview_area then SMODS.RunSelect.Functions.populate_preview_ui(self.key, choice, self.silent, true) end end end, set_default = function(self, choice) @@ -67,19 +68,27 @@ SMODS.RunSelectPage = SMODS.GameObject:extend({ return localize({set = self.type, key = selection, type = 'name_text'}) end, choose_random = function(self) - local selected = false local options = {} for i=1, #self.pool do if self.pool[i].unlocked then options[#options + 1] = self.pool[i].key end end - while not selected do - selected = pseudorandom_element(options, pseudoseed(os.time())) - if selected == SMODS.RunSelect.Setup.choices[self.key] and #options > 1 then selected = false end + if self.selection_limit > 1 then + for k,_ in pairs(SMODS.RunSelect.Setup.choices[self.key]) do + SMODS.RunSelect.Setup.choices[self.key][k] = nil + SMODS.RunSelect.Functions.populate_preview_ui(self.key, SMODS.RunSelect.Internals.preview_area.cards[1], self.silent, true) + end + end + for i=1, self.selection_limit do + local selected = false + while not selected do + selected = pseudorandom_element(options, pseudoseed(os.time())) + if (selected == SMODS.RunSelect.Setup.choices[self.key] or SMODS.RunSelect.Setup.choices[self.key][selected]) and #options > 1 then selected = false end + end + play_sound('whoosh1', math.random()*0.2 + 0.99, 0.35) + self:handle_choice({config = {center = {key = selected}}}) end - play_sound('whoosh1', math.random()*0.2 + 0.99, 0.35) - self:handle_choice({config = {center = {key = selected}}}) end }) diff --git a/src/utils/run_select.lua b/src/utils/run_select.lua index ac3ca69e7..c2c67ce61 100644 --- a/src/utils/run_select.lua +++ b/src/utils/run_select.lua @@ -373,7 +373,7 @@ function SMODS.RunSelect.Functions.build_selection_areas(key) SMODS.RunSelect.Internals.select_areas = {} for i=1, page_def.amount do SMODS.RunSelect.Internals.select_areas[i] = CardArea(G.ROOM.T.w, G.ROOM.T.h, dim.w, dim.h, - {card_limit = 5, type = page_def.area_type or 'title_2', highlight_limit = 0, deck_height = 0.75, thin_draw = 1, run_select = key}) + {card_limit = 5, type = page_def.area_type or 'title_2', highlight_limit = 0, deck_height = 0.75, thin_draw = 1, run_select = key, run_select_deck_preview = page_def.area_type == 'deck'}) end end @@ -514,8 +514,8 @@ function SMODS.RunSelect.Functions.build_preview_areas(key) end end - SMODS.RunSelect.Internals.preview_area = CardArea(15.475, 0, G.CARD_W * 1.5, G.CARD_H, - {card_limit = page_def.preview_size or page_def.selection_limit, type = page_def.area_type or 'title_2', highlight_limit = 0, run_select_deck_preview = page_def.key == 'deck_choice'}) + SMODS.RunSelect.Internals.preview_area = CardArea(15.475, 0, G.CARD_W * (page_def.selection_limit > 1 and 1.5 or 1), G.CARD_H, + {card_limit = page_def.preview_size or page_def.selection_limit, type = page_def.area_type or 'title_2', highlight_limit = 0, run_select_deck_preview = page_def.area_type == 'deck'}) SMODS.RunSelect.Internals.preview_area_holding = CardArea(15.475+2*G.CARD_W, -2*G.CARD_H, G.CARD_W, G.CARD_H, {card_limit = page_def.preview_size or page_def.selection_limit, type = page_def.area_type or 'title_2', highlight_limit = 0}) end @@ -525,6 +525,7 @@ function SMODS.RunSelect.Functions.update_preview_texts(page_def) for i, text in ipairs(preview_texts) do SMODS.RunSelect.Internals.preview_texts['preview_text_'..i] = text local dyna_text_container = G.OVERLAY_MENU:get_UIE_by_ID('preview_text_'..i) + if not dyna_text_container then return end dyna_text_container.config.object.scale = 0.7/math.max(1, string.len(text)/8) end end @@ -538,7 +539,7 @@ function SMODS.RunSelect.Functions.build_preview_ui(key, deck_preview) end local preview_area_node = {n=G.UIT.R, config = {align = 'tm'}, nodes = { - {n=G.UIT.O, config = {object = SMODS.RunSelect.Internals.preview_area}} + {n=G.UIT.O, config = {align = 'cm', object = SMODS.RunSelect.Internals.preview_area}} }} return {n=G.UIT.C, config = {align = "tm", padding = 0.1}, nodes ={ @@ -547,18 +548,14 @@ function SMODS.RunSelect.Functions.build_preview_ui(key, deck_preview) {n=G.UIT.O, config = {id = (deck_preview and 'deck_' or '')..'preview_text_1', object = DynaText({ string = {{ref_table = SMODS.RunSelect.Internals.preview_texts, ref_value = (deck_preview and 'deck_' or '')..'preview_text_1'}}, scale = 0.7/math.max(1, string.len(SMODS.RunSelect.Internals.preview_texts[(deck_preview and 'deck_' or '')..'preview_text_1'])/8), - colours = {G.C.GREY}, - pop_in_rate = 5, - silent = true + colours = {G.C.GREY}, pop_in_rate = 5, silent = true, non_recalc = true })}} }}, {n = G.UIT.R, config = {align = "cm", minh = 0.6, maxw = 2.8}, nodes = { {n=G.UIT.O, config = {id = (deck_preview and 'deck_' or '')..'preview_text_2', object = DynaText({ string = {{ref_table = SMODS.RunSelect.Internals.preview_texts, ref_value = (deck_preview and 'deck_' or '')..'preview_text_2'}}, scale = 0.7/math.max(1, string.len(SMODS.RunSelect.Internals.preview_texts[(deck_preview and 'deck_' or '')..'preview_text_2'])/8), - colours = {G.C.GREY}, - pop_in_rate = 5, - silent = true + colours = {G.C.GREY}, pop_in_rate = 5, silent = true, non_recalc = true })}} }}, {n = G.UIT.R, config = {align = "cm", minh = 0.2}}, @@ -609,7 +606,6 @@ function SMODS.RunSelect.Functions.populate_preview_ui(key, to_add, silent, _rem local card = page_def.create_selection_card and page_def:create_selection_card(type(to_add) == 'table' and to_add[j] or to_add, j, preview_area) or Card(preview_area.T.x, preview_area.T.y, card_size.w, card_size.h, nil, G.P_CENTERS[type(to_add) == 'table' and to_add[j] or to_add]) card.params.run_select_preview_card = page_def.key - if silent then preview_area:emplace(card) else @@ -623,7 +619,7 @@ function SMODS.RunSelect.Functions.populate_preview_ui(key, to_add, silent, _rem }), 'run_select') end end - if not silent then SMODS.RunSelect.Functions.update_preview_texts(page_def) end + SMODS.RunSelect.Functions.update_preview_texts(page_def) end function SMODS.RunSelect.Functions.build_stake_tower() @@ -913,7 +909,7 @@ function CardArea:align_cards() for k, card in ipairs(self.cards) do if not card.states.drag.is then card.T.x = self.T.x + 0.5*(self.T.w - card.T.w) - card.T.y = self.T.y + 0.5*(self.T.h - card.T.h) + self.shadow_parrallax.y*0.25/52*(#self.cards/2 - k) + card.T.y = self.T.y + 0.5*(self.T.h - card.T.h) + (self.shadow_parrallax.y*(self.config.deck_height or 0.25)/52*(#self.cards/2 - k)) end end else From 3c9740d91f7d4cd39b44ec06e076a5028e4db152 Mon Sep 17 00:00:00 2001 From: Eremel Date: Thu, 4 Jun 2026 22:53:39 +0100 Subject: [PATCH 4/4] fixes: uninstalled decks/stakes, cycling pages maintains choices, sprites get stuck when changing pages --- src/game_objects/runselectpage.lua | 18 ++++++++++++++---- src/utils/run_select.lua | 20 +++++++++++++++++--- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/game_objects/runselectpage.lua b/src/game_objects/runselectpage.lua index 3fdb09083..4b45089b5 100644 --- a/src/game_objects/runselectpage.lua +++ b/src/game_objects/runselectpage.lua @@ -61,7 +61,15 @@ SMODS.RunSelectPage = SMODS.GameObject:extend({ end end, set_default = function(self, choice) - return self.selection_limit > 1 and (type(choice) == 'table' and choice or {choice}) or choice + if self.selection_limit > 1 then + if type(choice) ~= 'table' then choice = {choice} end + local final = {} + for i, k in ipairs(choice) do + if G.P_CENTERS[k] then final[#final+1] = k end + end + return final + end + return G.P_CENTERS[choice] and choice end, selected_text = function(self, selection) if not selection then return end @@ -110,10 +118,11 @@ SMODS.RunSelectPage({ stack_size = 10, preview_size = 52, quick_start_text = function() - return localize({type = 'name_text', set = 'Back', key = G.PROFILES[G.SETTINGS.profile].last_choices.deck_choice or 'b_red'}) + if not G.P_CENTERS[G.PROFILES[G.SETTINGS.profile].last_choices.deck_choice] then G.PROFILES[G.SETTINGS.profile].last_choices.deck_choice = 'b_red' end + return localize({type = 'name_text', set = 'Back', key = G.PROFILES[G.SETTINGS.profile].last_choices.deck_choice}) end, set_default = function(self, choice) - return choice or 'b_red' + return G.P_CENTERS[choice] and choice or 'b_red' end, create_selection_card = function(self, card_key, card_number, area) local card = Card(area.T.x, area.T.y, G.CARD_W, G.CARD_H, nil, G.P_CENTERS[card_key] or G.P_CENTERS.b_red) @@ -142,7 +151,8 @@ SMODS.RunSelectPage({ end, sprite_size = {w = 0.99, h = 0.99}, quick_start_text = function() - return localize({type = 'name_text', set = 'Stake', key = G.P_CENTER_POOLS.Stake[G.PROFILES[G.SETTINGS.profile].last_choices.stake_choice or 1].key}) + if G.PROFILES[G.SETTINGS.profile].last_choices.stake_choice > #G.P_CENTER_POOLS.Stake then G.PROFILES[G.SETTINGS.profile].last_choices.stake_choice = 1 end + return localize({type = 'name_text', set = 'Stake', key = G.P_CENTER_POOLS.Stake[G.PROFILES[G.SETTINGS.profile].last_choices.stake_choice].key}) end, set_default = function(self, choice) if not choice or choice > #G.P_CENTER_POOLS.Stake then return 1 else return self.is_stake_unlocked(G.P_CENTER_POOLS.Stake[choice]) and choice or 1 end diff --git a/src/utils/run_select.lua b/src/utils/run_select.lua index c2c67ce61..90e83f03c 100644 --- a/src/utils/run_select.lua +++ b/src/utils/run_select.lua @@ -66,7 +66,7 @@ end function SMODS.RunSelect.Functions.create_page(key) local page_def = SMODS.RunSelect.Pages[key] - SMODS.RunSelect.Setup.choices[key] = page_def:set_default(G.PROFILES[G.SETTINGS.profile].last_choices[key]) + SMODS.RunSelect.Setup.choices[key] = SMODS.RunSelect.Setup.choices[key] or page_def:set_default(G.PROFILES[G.SETTINGS.profile].last_choices[key]) SMODS.RunSelect.Functions.build_selection_areas(key) local deck_preview, stake_tower, other_preview @@ -306,6 +306,7 @@ function SMODS.RunSelect.Functions.start_run(_quick_start) end G.PROFILES[G.SETTINGS.profile].last_choices = copy_table(run_args) + SMODS.RunSelect.Setup.choices = EMPTY(SMODS.RunSelect.Setup.choices) G:save_settings() run_args.deck_choice = {name = G.P_CENTERS[run_args.deck_choice].name} @@ -485,7 +486,7 @@ function SMODS.RunSelect.Functions.create_page_cycle(key, count_per_page) end local switch_func = function(args) - SMODS.RunSelect.Functions.clean_up() + SMODS.RunSelect.Functions.clean_up(true) SMODS.RunSelect.Functions.populate_selection_ui(key, args.to) end @@ -707,13 +708,26 @@ local function order_stake_chain(stake_chain, _stake) return ordered_chain end -function SMODS.RunSelect.Functions.clean_up() +function SMODS.RunSelect.Functions.clean_up(early) for j = 1, #SMODS.RunSelect.Internals.select_areas do if SMODS.RunSelect.Internals.select_areas[j].cards then remove_all(SMODS.RunSelect.Internals.select_areas[j].cards) SMODS.RunSelect.Internals.select_areas[j].cards = {} end end + if early then return end + if SMODS.RunSelect.Internals.stake_tower and SMODS.RunSelect.Internals.stake_tower.cards then + remove_all(SMODS.RunSelect.Internals.stake_tower.cards) + SMODS.RunSelect.Internals.stake_tower.cards = {} + remove_all(SMODS.RunSelect.Internals.stake_tower_holding.cards) + SMODS.RunSelect.Internals.stake_tower_holding.cards = {} + end + if SMODS.RunSelect.Internals.preview_area and SMODS.RunSelect.Internals.preview_area.cards then + remove_all(SMODS.RunSelect.Internals.preview_area.cards) + SMODS.RunSelect.Internals.preview_area.cards = {} + remove_all(SMODS.RunSelect.Internals.preview_area_holding.cards) + SMODS.RunSelect.Internals.preview_area_holding.cards = {} + end end -- Function Hooks