Source code for bauiv1lib.coop.browser

# Released under the MIT License. See LICENSE for details.
#
"""UI for browsing available co-op levels/games/etc."""
# FIXME: Break this up.
# pylint: disable=too-many-lines

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, override


import bauiv1 as bui

if TYPE_CHECKING:
    from typing import Any

    from bauiv1lib.coop.tournamentbutton import TournamentButton

HARD_REQUIRES_PRO = False


[docs] class CoopBrowserWindow(bui.MainWindow): """Window for browsing co-op levels/games/etc.""" def __init__( self, transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-statements # pylint: disable=too-many-locals # pylint: disable=cyclic-import plus = bui.app.plus assert plus is not None # Preload some modules we use in a background thread so we won't # have a visual hitch when the user taps them. bui.app.threadpool.submit_no_wait(self._preload_modules) bui.set_analytics_screen('Coop Window') app = bui.app classic = app.classic assert classic is not None cfg = app.config # Quick note to players that tourneys won't work in ballistica # core builds. (need to split the word so it won't get subbed # out) if 'ballistica' + 'kit' == bui.appname() and bui.do_once(): bui.apptimer( 1.0, lambda: bui.screenmessage( bui.Lstr(resource='noTournamentsInTestBuildText'), color=(1, 1, 0), ), ) # Try to recreate the same number of buttons we had last time so our # re-selection code works. self._tournament_button_count = app.config.get('Tournament Rows', 0) assert isinstance(self._tournament_button_count, int) self.star_tex = bui.gettexture('star') self.lsbt = bui.getmesh('level_select_button_transparent') self.lsbo = bui.getmesh('level_select_button_opaque') self.a_outline_tex = bui.gettexture('achievementOutline') self.a_outline_mesh = bui.getmesh('achievementOutline') self._campaign_sub_container: bui.Widget | None = None self._tournament_info_button: bui.Widget | None = None self._easy_button: bui.Widget | None = None self._hard_button: bui.Widget | None = None self._hard_button_lock_image: bui.Widget | None = None self._campaign_percent_text: bui.Widget | None = None uiscale = app.ui_v1.uiscale self._width = 1600 if uiscale is bui.UIScale.SMALL else 1120 self._height = ( 1200 if uiscale is bui.UIScale.SMALL else 730 if uiscale is bui.UIScale.MEDIUM else 800 ) self._r = 'coopSelectWindow' top_extra = 0 if uiscale is bui.UIScale.SMALL else 0 self._tourney_data_up_to_date = False self._campaign_difficulty = plus.get_v1_account_misc_val( 'campaignDifficulty', 'easy' ) if ( self._campaign_difficulty == 'hard' and HARD_REQUIRES_PRO and not classic.accounts.have_pro_options() ): plus.add_v1_account_transaction( { 'type': 'SET_MISC_VAL', 'name': 'campaignDifficulty', 'value': 'easy', } ) self._campaign_difficulty = 'easy' # Do some fancy math to fill all available screen area up to the # size of our backing container. This lets us fit to the exact # screen shape at small ui scale. screensize = bui.get_virtual_screen_size() scale = ( 1.5 if uiscale is bui.UIScale.SMALL else 0.8 if uiscale is bui.UIScale.MEDIUM else 0.75 ) # Calc screen size in our local container space and clamp to a # bit smaller than our container size. target_width = min(self._width - 120, screensize[0] / scale) target_height = min(self._height - 120, screensize[1] / scale) # To get top/left coords, go to the center of our window and # offset by half the width/height of our target area. yoffs = 0.5 * self._height + 0.5 * target_height + 30.0 self._scroll_width = target_width self._scroll_height = target_height - 40 self._scroll_bottom = yoffs - 70 - self._scroll_height super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), toolbar_visibility='menu_full', scale=scale, ), transition=transition, origin_widget=origin_widget, # We're affected by screen size only at small ui-scale. refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL, ) if uiscale is bui.UIScale.SMALL: self._back_button = bui.get_special_widget('back_button') bui.containerwidget( edit=self._root_widget, on_cancel_call=self.main_window_back ) else: self._back_button = bui.buttonwidget( parent=self._root_widget, position=(75, yoffs - 48.0), size=(60, 50), scale=1.2, autoselect=True, label=bui.charstr(bui.SpecialChar.BACK), button_type='backSmall', on_activate_call=self.main_window_back, ) bui.containerwidget( edit=self._root_widget, cancel_button=self._back_button ) self._last_tournament_query_time: float | None = None self._last_tournament_query_response_time: float | None = None self._doing_tournament_query = False self._selected_campaign_level = cfg.get( 'Selected Coop Campaign Level', None ) self._selected_custom_level = cfg.get( 'Selected Coop Custom Level', None ) if uiscale is bui.UIScale.SMALL: tmaxw = 130 if bui.get_virtual_screen_size()[0] < 1320 else 175 else: tmaxw = 300 # Don't want initial construction affecting our last-selected. self._do_selection_callbacks = False bui.textwidget( parent=self._root_widget, position=( self._width * 0.5, yoffs - (50 if uiscale is bui.UIScale.SMALL else 24), ), size=(0, 0), text=bui.Lstr( resource='playModes.singlePlayerCoopText', fallback_resource='playModes.coopText', ), h_align='center', color=app.ui_v1.title_color, scale=0.85 if uiscale is bui.UIScale.SMALL else 1.5, maxwidth=tmaxw, v_align='center', ) self._selected_row = cfg.get('Selected Coop Row', None) self._subcontainerwidth = 800.0 self._subcontainerheight = 1400.0 self._scrollwidget = bui.scrollwidget( parent=self._root_widget, highlight=False, size=(self._scroll_width, self._scroll_height), position=( self._width * 0.5 - self._scroll_width * 0.5, self._scroll_bottom, ), simple_culling_v=10.0, claims_left_right=True, selection_loops_to_parent=True, border_opacity=0.4, ) if uiscale is bui.UIScale.SMALL: blotchwidth = 500.0 blotchheight = 200.0 bimg = bui.imagewidget( parent=self._root_widget, texture=bui.gettexture('uiAtlas'), mesh_transparent=bui.getmesh('windowBGBlotch'), position=( self._width * 0.5 - self._scroll_width * 0.5 + 60.0 - blotchwidth * 0.5, self._scroll_bottom - blotchheight * 0.5, ), size=(blotchwidth, blotchheight), color=(0.4, 0.37, 0.49), # color=(1, 0, 0), ) bui.widget(edit=bimg, depth_range=(0.9, 1.0)) bimg = bui.imagewidget( parent=self._root_widget, texture=bui.gettexture('uiAtlas'), mesh_transparent=bui.getmesh('windowBGBlotch'), position=( self._width * 0.5 + self._scroll_width * 0.5 - 60.0 - blotchwidth * 0.5, self._scroll_bottom - blotchheight * 0.5, ), size=(blotchwidth, blotchheight), color=(0.4, 0.37, 0.49), # color=(1, 0, 0), ) bui.widget(edit=bimg, depth_range=(0.9, 1.0)) self._subcontainer: bui.Widget | None = None # Take note of our account state; we'll refresh later if this changes. self._account_state_num = plus.get_v1_account_state_num() # Same for fg/bg state. self._fg_state = app.fg_state self._refresh() self._restore_state() # Even though we might display cached tournament data immediately, we # don't consider it valid until we've pinged. # the server for an update self._tourney_data_up_to_date = False # If we've got a cached tournament list for our account and info for # each one of those tournaments, go ahead and display it as a # starting point. if ( classic.accounts.account_tournament_list is not None and classic.accounts.account_tournament_list[0] == plus.get_v1_account_state_num() and all( t_id in classic.accounts.tournament_info for t_id in classic.accounts.account_tournament_list[1] ) ): tourney_data = [ classic.accounts.tournament_info[t_id] for t_id in classic.accounts.account_tournament_list[1] ] self._update_for_data(tourney_data) # This will pull new data periodically, update timers, etc. self._update_timer = bui.AppTimer( 1.0, bui.WeakCall(self._update), repeat=True ) self._update()
[docs] @override def get_main_window_state(self) -> bui.MainWindowState: # Support recreating our window for back/refresh purposes. cls = type(self) return bui.BasicMainWindowState( create_call=lambda transition, origin_widget: cls( transition=transition, origin_widget=origin_widget ) )
[docs] @override def on_main_window_close(self) -> None: self._save_state()
@staticmethod def _preload_modules() -> None: """Preload modules we use; avoids hitches (called in bg thread).""" # pylint: disable=cyclic-import import bauiv1lib.purchase as _unused1 import bauiv1lib.coop.gamebutton as _unused2 import bauiv1lib.confirm as _unused3 import bauiv1lib.account as _unused4 import bauiv1lib.league.rankwindow as _unused5 import bauiv1lib.store.browser as _unused6 import bauiv1lib.account.viewer as _unused7 import bauiv1lib.tournamentscores as _unused8 import bauiv1lib.tournamententry as _unused9 import bauiv1lib.play as _unused10 import bauiv1lib.coop.tournamentbutton as _unused11 def _update(self) -> None: plus = bui.app.plus assert plus is not None # Do nothing if we've somehow outlived our actual UI. if not self._root_widget: return cur_time = bui.apptime() # If its been a while since we got a tournament update, consider # the data invalid (prevents us from joining tournaments if our # internet connection goes down for a while). if ( self._last_tournament_query_response_time is None or bui.apptime() - self._last_tournament_query_response_time > 60.0 * 2 ): self._tourney_data_up_to_date = False # If our account login state has changed, do a # full request. account_state_num = plus.get_v1_account_state_num() if account_state_num != self._account_state_num: self._account_state_num = account_state_num self._save_state() self._refresh() # Also encourage a new tournament query since this will clear out # our current results. if not self._doing_tournament_query: self._last_tournament_query_time = None # If we've been backgrounded/foregrounded, invalidate our # tournament entries (they will be refreshed below asap). if self._fg_state != bui.app.fg_state: self._tourney_data_up_to_date = False # Send off a new tournament query if its been long enough or whatnot. if not self._doing_tournament_query and ( self._last_tournament_query_time is None or cur_time - self._last_tournament_query_time > 30.0 or self._fg_state != bui.app.fg_state ): self._fg_state = bui.app.fg_state self._last_tournament_query_time = cur_time self._doing_tournament_query = True plus.tournament_query( args={'source': 'coop window refresh', 'numScores': 1}, callback=bui.WeakCall(self._on_tournament_query_response), ) # Decrement time on our tournament buttons. ads_enabled = plus.have_incentivized_ad() for tbtn in self._tournament_buttons: tbtn.time_remaining = max(0, tbtn.time_remaining - 1) if tbtn.time_remaining_value_text is not None: bui.textwidget( edit=tbtn.time_remaining_value_text, text=( bui.timestring(tbtn.time_remaining, centi=False) if ( tbtn.has_time_remaining and self._tourney_data_up_to_date ) else '-' ), ) # Also adjust the ad icon visibility. if tbtn.allow_ads and plus.has_video_ads(): bui.imagewidget( edit=tbtn.entry_fee_ad_image, opacity=1.0 if ads_enabled else 0.25, ) bui.textwidget( edit=tbtn.entry_fee_text_remaining, color=(0.6, 0.6, 0.6, 1 if ads_enabled else 0.2), ) self._update_hard_mode_lock_image() def _update_hard_mode_lock_image(self) -> None: assert bui.app.classic is not None try: bui.imagewidget( edit=self._hard_button_lock_image, opacity=( 0.0 if ( (not HARD_REQUIRES_PRO) or bui.app.classic.accounts.have_pro_options() ) else 1.0 ), ) except Exception: logging.exception('Error updating campaign lock.') def _update_for_data(self, data: list[dict[str, Any]] | None) -> None: # If the number of tournaments or challenges in the data differs # from our current arrangement, refresh with the new number. if (data is None and self._tournament_button_count != 0) or ( data is not None and (len(data) != self._tournament_button_count) ): self._tournament_button_count = len(data) if data is not None else 0 bui.app.config['Tournament Rows'] = self._tournament_button_count self._refresh() # Update all of our tourney buttons based on whats in data. for i, tbtn in enumerate(self._tournament_buttons): assert data is not None tbtn.update_for_data(data[i]) def _on_tournament_query_response( self, data: dict[str, Any] | None ) -> None: plus = bui.app.plus assert plus is not None assert bui.app.classic is not None accounts = bui.app.classic.accounts if data is not None: tournament_data = data['t'] # This used to be the whole payload. self._last_tournament_query_response_time = bui.apptime() else: tournament_data = None # Keep our cached tourney info up to date. if data is not None: self._tourney_data_up_to_date = True accounts.cache_tournament_info(tournament_data) # Also cache the current tourney list/order for this account. accounts.account_tournament_list = ( plus.get_v1_account_state_num(), [e['tournamentID'] for e in tournament_data], ) self._doing_tournament_query = False self._update_for_data(tournament_data) def _set_campaign_difficulty(self, difficulty: str) -> None: # pylint: disable=cyclic-import from bauiv1lib.purchase import PurchaseWindow plus = bui.app.plus assert plus is not None assert bui.app.classic is not None if difficulty != self._campaign_difficulty: if ( difficulty == 'hard' and HARD_REQUIRES_PRO and not bui.app.classic.accounts.have_pro_options() ): PurchaseWindow(items=['pro']) return bui.getsound('gunCocking').play() if difficulty not in ('easy', 'hard'): print('ERROR: invalid campaign difficulty:', difficulty) difficulty = 'easy' self._campaign_difficulty = difficulty plus.add_v1_account_transaction( { 'type': 'SET_MISC_VAL', 'name': 'campaignDifficulty', 'value': difficulty, } ) self._refresh_campaign_row() else: bui.getsound('click01').play() def _refresh_campaign_row(self) -> None: # pylint: disable=too-many-locals # pylint: disable=too-many-statements # pylint: disable=cyclic-import from bauiv1lib.coop.gamebutton import GameButton parent_widget = self._campaign_sub_container # Clear out anything in the parent widget already. assert parent_widget is not None for child in parent_widget.get_children(): child.delete() next_widget_down = self._tournament_info_button h = 0 v2 = -2 sel_color = (0.75, 0.85, 0.5) sel_color_hard = (0.4, 0.7, 0.2) un_sel_color = (0.5, 0.5, 0.5) sel_textcolor = (2, 2, 0.8) un_sel_textcolor = (0.6, 0.6, 0.6) self._easy_button = bui.buttonwidget( parent=parent_widget, position=(h + 30, v2 + 105), size=(120, 70), label=bui.Lstr(resource='difficultyEasyText'), button_type='square', autoselect=True, enable_sound=False, on_activate_call=bui.Call(self._set_campaign_difficulty, 'easy'), on_select_call=bui.Call(self.sel_change, 'campaign', 'easyButton'), color=( sel_color if self._campaign_difficulty == 'easy' else un_sel_color ), textcolor=( sel_textcolor if self._campaign_difficulty == 'easy' else un_sel_textcolor ), ) bui.widget(edit=self._easy_button, show_buffer_left=100) if self._selected_campaign_level == 'easyButton': bui.containerwidget( edit=parent_widget, selected_child=self._easy_button, visible_child=self._easy_button, ) lock_tex = bui.gettexture('lock') self._hard_button = bui.buttonwidget( parent=parent_widget, position=(h + 30, v2 + 32), size=(120, 70), label=bui.Lstr(resource='difficultyHardText'), button_type='square', autoselect=True, enable_sound=False, on_activate_call=bui.Call(self._set_campaign_difficulty, 'hard'), on_select_call=bui.Call(self.sel_change, 'campaign', 'hardButton'), color=( sel_color_hard if self._campaign_difficulty == 'hard' else un_sel_color ), textcolor=( sel_textcolor if self._campaign_difficulty == 'hard' else un_sel_textcolor ), ) self._hard_button_lock_image = bui.imagewidget( parent=parent_widget, size=(30, 30), draw_controller=self._hard_button, position=(h + 30 - 10, v2 + 32 + 70 - 35), texture=lock_tex, ) self._update_hard_mode_lock_image() bui.widget(edit=self._hard_button, show_buffer_left=100) if self._selected_campaign_level == 'hardButton': bui.containerwidget( edit=parent_widget, selected_child=self._hard_button, visible_child=self._hard_button, ) bui.widget(edit=self._hard_button, down_widget=next_widget_down) h_spacing = 200 campaign_buttons = [] if self._campaign_difficulty == 'easy': campaignname = 'Easy' else: campaignname = 'Default' items = [ campaignname + ':Onslaught Training', campaignname + ':Rookie Onslaught', campaignname + ':Rookie Football', campaignname + ':Pro Onslaught', campaignname + ':Pro Football', campaignname + ':Pro Runaround', campaignname + ':Uber Onslaught', campaignname + ':Uber Football', campaignname + ':Uber Runaround', ] items += [campaignname + ':The Last Stand'] if self._selected_campaign_level is None: self._selected_campaign_level = items[0] h = 150 for i in items: is_last_sel = i == self._selected_campaign_level campaign_buttons.append( GameButton( self, parent_widget, i, h, v2, is_last_sel, 'campaign' ).get_button() ) h += h_spacing bui.widget(edit=campaign_buttons[0], left_widget=self._easy_button) bui.widget( edit=self._easy_button, left_widget=self._back_button, up_widget=self._back_button, ) bui.widget(edit=self._hard_button, left_widget=self._back_button) for btn in campaign_buttons: bui.widget( edit=btn, up_widget=self._back_button, ) for btn in campaign_buttons: bui.widget(edit=btn, down_widget=next_widget_down) # Update our existing percent-complete text. assert bui.app.classic is not None campaign = bui.app.classic.getcampaign(campaignname) levels = campaign.levels levels_complete = sum((1 if l.complete else 0) for l in levels) # Last level cant be completed; hence the -1. progress = min(1.0, float(levels_complete) / (len(levels) - 1)) p_str = str(int(progress * 100.0)) + '%' self._campaign_percent_text = bui.textwidget( edit=self._campaign_percent_text, text=bui.Lstr( value='${C} (${P})', subs=[ ('${C}', bui.Lstr(resource=f'{self._r}.campaignText')), ('${P}', p_str), ], ), ) def _on_tournament_info_press(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.confirm import ConfirmWindow txt = bui.Lstr(resource=f'{self._r}.tournamentInfoText') ConfirmWindow( txt, cancel_button=False, width=550, height=260, origin_widget=self._tournament_info_button, ) def _refresh(self) -> None: # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=cyclic-import from bauiv1lib.coop.gamebutton import GameButton from bauiv1lib.coop.tournamentbutton import TournamentButton plus = bui.app.plus assert plus is not None assert bui.app.classic is not None # (Re)create the sub-container if need be. if self._subcontainer is not None: self._subcontainer.delete() tourney_row_height = 200 self._subcontainerheight = ( 700 + self._tournament_button_count * tourney_row_height ) self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(self._subcontainerwidth, self._subcontainerheight), background=False, claims_left_right=True, selection_loops_to_parent=True, ) bui.containerwidget( edit=self._root_widget, selected_child=self._scrollwidget ) w_parent = self._subcontainer h_base = 6 v = self._subcontainerheight - 90 self._campaign_percent_text = bui.textwidget( parent=w_parent, position=(h_base + 27, v + 30), size=(0, 0), text='', h_align='left', v_align='center', color=bui.app.ui_v1.title_color, scale=1.1, ) row_v_show_buffer = 80 v -= 198 h_scroll = bui.hscrollwidget( parent=w_parent, size=(self._scroll_width, 205), position=(-5, v), simple_culling_h=70, highlight=False, border_opacity=0.0, color=(0.45, 0.4, 0.5), on_select_call=lambda: self._on_row_selected('campaign'), ) self._campaign_h_scroll = h_scroll bui.widget( edit=h_scroll, show_buffer_top=row_v_show_buffer, show_buffer_bottom=row_v_show_buffer, autoselect=True, ) if self._selected_row == 'campaign': bui.containerwidget( edit=w_parent, selected_child=h_scroll, visible_child=h_scroll ) bui.containerwidget(edit=h_scroll, claims_left_right=True) self._campaign_sub_container = bui.containerwidget( parent=h_scroll, size=(180 + 200 * 10, 200), background=False ) # Tournaments self._tournament_buttons: list[TournamentButton] = [] v -= 53 # FIXME shouldn't use hard-coded strings here. txt = bui.Lstr( resource='tournamentsText', fallback_resource='tournamentText' ).evaluate() t_width = bui.get_string_width(txt, suppress_warning=True) bui.textwidget( parent=w_parent, position=(h_base + 27, v + 30), size=(0, 0), text=txt, h_align='left', v_align='center', color=bui.app.ui_v1.title_color, scale=1.1, ) self._tournament_info_button = bui.buttonwidget( parent=w_parent, label='?', size=(20, 20), text_scale=0.6, position=(h_base + 27 + t_width * 1.1 + 15, v + 18), button_type='square', color=(0.6, 0.5, 0.65), textcolor=(0.7, 0.6, 0.75), autoselect=True, up_widget=self._campaign_h_scroll, left_widget=self._back_button, on_activate_call=self._on_tournament_info_press, ) bui.widget( edit=self._tournament_info_button, right_widget=self._tournament_info_button, ) # Say 'unavailable' if there are zero tournaments, and if we're # not signed in add that as well (that's probably why we see no # tournaments). if self._tournament_button_count == 0: unavailable_text = bui.Lstr(resource='unavailableText') if plus.get_v1_account_state() != 'signed_in': unavailable_text = bui.Lstr( value='${A} (${B})', subs=[ ('${A}', unavailable_text), ('${B}', bui.Lstr(resource='notSignedInText')), ], ) bui.textwidget( parent=w_parent, position=(h_base + 47, v), size=(0, 0), text=unavailable_text, h_align='left', v_align='center', color=bui.app.ui_v1.title_color, scale=0.9, ) v -= 40 v -= 198 tournament_h_scroll = None if self._tournament_button_count > 0: for i in range(self._tournament_button_count): tournament_h_scroll = h_scroll = bui.hscrollwidget( parent=w_parent, size=(self._scroll_width, 205), position=(-5, v), highlight=False, border_opacity=0.0, color=(0.45, 0.4, 0.5), on_select_call=bui.Call( self._on_row_selected, 'tournament' + str(i + 1) ), ) bui.widget( edit=h_scroll, show_buffer_top=row_v_show_buffer, show_buffer_bottom=row_v_show_buffer, autoselect=True, ) if self._selected_row == 'tournament' + str(i + 1): bui.containerwidget( edit=w_parent, selected_child=h_scroll, visible_child=h_scroll, ) bui.containerwidget(edit=h_scroll, claims_left_right=True) sc2 = bui.containerwidget( parent=h_scroll, size=(self._scroll_width - 24, 200), background=False, ) h = 0 v2 = -2 is_last_sel = True self._tournament_buttons.append( TournamentButton( sc2, h, v2, is_last_sel, on_pressed=bui.WeakCall(self.run_tournament), ) ) v -= 200 # Custom Games. (called 'Practice' in UI these days). v -= 50 bui.textwidget( parent=w_parent, position=(h_base + 27, v + 30 + 198), size=(0, 0), text=bui.Lstr( resource='practiceText', fallback_resource='coopSelectWindow.customText', ), h_align='left', v_align='center', color=bui.app.ui_v1.title_color, scale=1.1, ) items = [ 'Challenges:Infinite Onslaught', 'Challenges:Infinite Runaround', 'Challenges:Ninja Fight', 'Challenges:Pro Ninja Fight', 'Challenges:Meteor Shower', 'Challenges:Target Practice B', 'Challenges:Target Practice', ] # Show easter-egg-hunt either if its easter or we own it. if plus.get_v1_account_misc_read_val( 'easter', False ) or plus.get_v1_account_product_purchased('games.easter_egg_hunt'): items = [ 'Challenges:Easter Egg Hunt', 'Challenges:Pro Easter Egg Hunt', ] + items # If we've defined custom games, put them at the beginning. if bui.app.classic.custom_coop_practice_games: items = bui.app.classic.custom_coop_practice_games + items self._custom_h_scroll = custom_h_scroll = h_scroll = bui.hscrollwidget( parent=w_parent, size=(self._scroll_width, 205), position=(-5, v), highlight=False, border_opacity=0.0, color=(0.45, 0.4, 0.5), on_select_call=bui.Call(self._on_row_selected, 'custom'), ) bui.widget( edit=h_scroll, show_buffer_top=row_v_show_buffer, show_buffer_bottom=1.5 * row_v_show_buffer, autoselect=True, ) if self._selected_row == 'custom': bui.containerwidget( edit=w_parent, selected_child=h_scroll, visible_child=h_scroll ) bui.containerwidget(edit=h_scroll, claims_left_right=True) sc2 = bui.containerwidget( parent=h_scroll, size=(max(self._scroll_width - 24, 30 + 200 * len(items)), 200), background=False, ) h_spacing = 200 self._custom_buttons: list[GameButton] = [] h = 0 v2 = -2 for item in items: is_last_sel = item == self._selected_custom_level self._custom_buttons.append( GameButton(self, sc2, item, h, v2, is_last_sel, 'custom') ) h += h_spacing # We can't fill in our campaign row until tourney buttons are in place. # (for wiring up) self._refresh_campaign_row() for i, tbutton in enumerate(self._tournament_buttons): bui.widget( edit=tbutton.button, up_widget=( self._tournament_info_button if i == 0 else self._tournament_buttons[i - 1].button ), down_widget=( self._tournament_buttons[(i + 1)].button if i + 1 < len(self._tournament_buttons) else custom_h_scroll ), left_widget=self._back_button, ) bui.widget( edit=tbutton.more_scores_button, down_widget=( self._tournament_buttons[(i + 1)].current_leader_name_text if i + 1 < len(self._tournament_buttons) else custom_h_scroll ), ) bui.widget( edit=tbutton.current_leader_name_text, up_widget=( self._tournament_info_button if i == 0 else self._tournament_buttons[i - 1].more_scores_button ), ) for i, btn in enumerate(self._custom_buttons): try: bui.widget( edit=btn.get_button(), up_widget=( tournament_h_scroll if self._tournament_buttons else self._tournament_info_button ), ) if i == 0: bui.widget( edit=btn.get_button(), left_widget=self._back_button ) except Exception: logging.exception('Error wiring up custom buttons.') # There's probably several 'onSelected' callbacks pushed onto the # event queue.. we need to push ours too so we're enabled *after* them. bui.pushcall(self._enable_selectable_callback) def _on_row_selected(self, row: str) -> None: if self._do_selection_callbacks: if self._selected_row != row: self._selected_row = row def _enable_selectable_callback(self) -> None: self._do_selection_callbacks = True
[docs] def is_tourney_data_up_to_date(self) -> bool: """Return whether our tourney data is up to date.""" return self._tourney_data_up_to_date
[docs] def run_game( self, game: str, origin_widget: bui.Widget | None = None ) -> None: """Run the provided game.""" from efro.util import strict_partial from bauiv1lib.confirm import ConfirmWindow classic = bui.app.classic assert classic is not None if classic.chest_dock_full: ConfirmWindow( bui.Lstr(resource='chests.slotsFullWarningText'), width=550, height=140, ok_text=bui.Lstr(resource='continueText'), origin_widget=origin_widget, action=strict_partial( self._run_game, game=game, origin_widget=origin_widget ), ) else: self._run_game(game=game, origin_widget=origin_widget)
def _run_game( self, game: str, origin_widget: bui.Widget | None = None ) -> None: """Run the provided game.""" # pylint: disable=cyclic-import from bauiv1lib.confirm import ConfirmWindow from bauiv1lib.purchase import PurchaseWindow from bauiv1lib.account.signin import show_sign_in_prompt plus = bui.app.plus assert plus is not None assert bui.app.classic is not None args: dict[str, Any] = {} if game == 'Easy:The Last Stand': ConfirmWindow( bui.Lstr( resource='difficultyHardUnlockOnlyText', fallback_resource='difficultyHardOnlyText', ), cancel_button=False, width=460, height=130, ) return required_purchases = bui.app.classic.required_purchases_for_game(game) # Show pop-up to allow purchasing any required stuff we don't have. for purchase in required_purchases: if not plus.get_v1_account_product_purchased(purchase): if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() else: PurchaseWindow( items=[purchase], origin_widget=origin_widget ) return self._save_state() if bui.app.classic.launch_coop_game(game, args=args): bui.containerwidget(edit=self._root_widget, transition='out_left')
[docs] def run_tournament(self, tournament_button: TournamentButton) -> None: """Run the provided tournament game.""" # pylint: disable=too-many-return-statements from bauiv1lib.purchase import PurchaseWindow from bauiv1lib.account.signin import show_sign_in_prompt from bauiv1lib.tournamententry import TournamentEntryWindow plus = bui.app.plus assert plus is not None classic = bui.app.classic assert classic is not None if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return if bui.workspaces_in_use(): bui.screenmessage( bui.Lstr(resource='tournamentsDisabledWorkspaceText'), color=(1, 0, 0), ) bui.getsound('error').play() return if not self._tourney_data_up_to_date: bui.screenmessage( bui.Lstr(resource='tournamentCheckingStateText'), color=(1, 1, 0), ) bui.getsound('error').play() return if tournament_button.tournament_id is None: bui.screenmessage( bui.Lstr(resource='internal.unavailableNoConnectionText'), color=(1, 0, 0), ) bui.getsound('error').play() return if tournament_button.required_league is not None: bui.screenmessage( bui.Lstr( resource='league.tournamentLeagueText', subs=[ ( '${NAME}', bui.Lstr( translate=( 'leagueNames', tournament_button.required_league, ) ), ) ], ), color=(1, 0, 0), ) bui.getsound('error').play() return if tournament_button.game is not None and not classic.is_game_unlocked( tournament_button.game ): required_purchases = classic.required_purchases_for_game( tournament_button.game ) # We gotta be missing *something* if its locked. assert required_purchases for purchase in required_purchases: if not plus.get_v1_account_product_purchased(purchase): if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() else: PurchaseWindow( items=[purchase], origin_widget=tournament_button.button, ) return # assert required_purchases # if plus.get_v1_account_state() != 'signed_in': # show_sign_in_prompt() # else: # # Hmm; just show the first requirement. They can come # # back to see more after they purchase the first. # PurchaseWindow( # items=[required_purchases[0]], # origin_widget=tournament_button.button, # ) # return if tournament_button.time_remaining <= 0: bui.screenmessage( bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) ) bui.getsound('error').play() return self._save_state() assert tournament_button.tournament_id is not None TournamentEntryWindow( tournament_id=tournament_button.tournament_id, position=tournament_button.button.get_screen_space_center(), )
def _save_state(self) -> None: cfg = bui.app.config try: sel = self._root_widget.get_selected_child() if sel == self._back_button: sel_name = 'Back' elif sel == self._scrollwidget: sel_name = 'Scroll' else: raise ValueError('unrecognized selection') assert bui.app.classic is not None bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name} except Exception: logging.exception('Error saving state for %s.', self) cfg['Selected Coop Row'] = self._selected_row cfg['Selected Coop Custom Level'] = self._selected_custom_level cfg['Selected Coop Campaign Level'] = self._selected_campaign_level cfg.commit() def _restore_state(self) -> None: try: assert bui.app.classic is not None sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get( 'sel_name' ) if sel_name == 'Back': sel = self._back_button elif sel_name == 'Scroll': sel = self._scrollwidget else: sel = self._scrollwidget bui.containerwidget(edit=self._root_widget, selected_child=sel) except Exception: logging.exception('Error restoring state for %s.', self)
[docs] def sel_change(self, row: str, game: str) -> None: """(internal)""" if self._do_selection_callbacks: if row == 'custom': self._selected_custom_level = game elif row == 'campaign': self._selected_campaign_level = game
# Docs-generation hack; import some stuff that we likely only forward-declared # in our actual source code so that docs tools can find it. from typing import (Coroutine, Any, Literal, Callable, Generator, Awaitable, Sequence, Self) import asyncio from concurrent.futures import Future from pathlib import Path from enum import Enum