bauiv1lib.coop.browser

UI for browsing available co-op levels/games/etc.

   1# Released under the MIT License. See LICENSE for details.
   2#
   3"""UI for browsing available co-op levels/games/etc."""
   4# FIXME: Break this up.
   5# pylint: disable=too-many-lines
   6
   7from __future__ import annotations
   8
   9import logging
  10from threading import Thread
  11from typing import TYPE_CHECKING
  12
  13from bauiv1lib.store.button import StoreButton
  14from bauiv1lib.league.rankbutton import LeagueRankButton
  15from bauiv1lib.store.browser import StoreBrowserWindow
  16import bauiv1 as bui
  17
  18if TYPE_CHECKING:
  19    from typing import Any
  20
  21    from bauiv1lib.coop.tournamentbutton import TournamentButton
  22
  23
  24class CoopBrowserWindow(bui.Window):
  25    """Window for browsing co-op levels/games/etc."""
  26
  27    def __init__(
  28        self,
  29        transition: str | None = 'in_right',
  30        origin_widget: bui.Widget | None = None,
  31    ):
  32        # pylint: disable=too-many-statements
  33        # pylint: disable=cyclic-import
  34
  35        plus = bui.app.plus
  36        assert plus is not None
  37
  38        # Preload some modules we use in a background thread so we won't
  39        # have a visual hitch when the user taps them.
  40        Thread(target=self._preload_modules).start()
  41
  42        bui.set_analytics_screen('Coop Window')
  43
  44        app = bui.app
  45        assert app.classic is not None
  46        cfg = app.config
  47
  48        # Quick note to players that tourneys won't work in ballistica
  49        # core builds. (need to split the word so it won't get subbed out)
  50        if 'ballistica' + 'kit' == bui.appname():
  51            bui.apptimer(
  52                1.0,
  53                lambda: bui.screenmessage(
  54                    bui.Lstr(resource='noTournamentsInTestBuildText'),
  55                    color=(1, 1, 0),
  56                ),
  57            )
  58
  59        # If they provided an origin-widget, scale up from that.
  60        scale_origin: tuple[float, float] | None
  61        if origin_widget is not None:
  62            self._transition_out = 'out_scale'
  63            scale_origin = origin_widget.get_screen_space_center()
  64            transition = 'in_scale'
  65        else:
  66            self._transition_out = 'out_right'
  67            scale_origin = None
  68
  69        # Try to recreate the same number of buttons we had last time so our
  70        # re-selection code works.
  71        self._tournament_button_count = app.config.get('Tournament Rows', 0)
  72        assert isinstance(self._tournament_button_count, int)
  73
  74        self.star_tex = bui.gettexture('star')
  75        self.lsbt = bui.getmesh('level_select_button_transparent')
  76        self.lsbo = bui.getmesh('level_select_button_opaque')
  77        self.a_outline_tex = bui.gettexture('achievementOutline')
  78        self.a_outline_mesh = bui.getmesh('achievementOutline')
  79        self._campaign_sub_container: bui.Widget | None = None
  80        self._tournament_info_button: bui.Widget | None = None
  81        self._easy_button: bui.Widget | None = None
  82        self._hard_button: bui.Widget | None = None
  83        self._hard_button_lock_image: bui.Widget | None = None
  84        self._campaign_percent_text: bui.Widget | None = None
  85
  86        assert bui.app.classic is not None
  87        uiscale = bui.app.ui_v1.uiscale
  88        self._width = 1320 if uiscale is bui.UIScale.SMALL else 1120
  89        self._x_inset = x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
  90        self._height = (
  91            657
  92            if uiscale is bui.UIScale.SMALL
  93            else 730
  94            if uiscale is bui.UIScale.MEDIUM
  95            else 800
  96        )
  97        app.ui_v1.set_main_menu_location('Coop Select')
  98        self._r = 'coopSelectWindow'
  99        top_extra = 20 if uiscale is bui.UIScale.SMALL else 0
 100
 101        self._tourney_data_up_to_date = False
 102
 103        self._campaign_difficulty = plus.get_v1_account_misc_val(
 104            'campaignDifficulty', 'easy'
 105        )
 106
 107        super().__init__(
 108            root_widget=bui.containerwidget(
 109                size=(self._width, self._height + top_extra),
 110                toolbar_visibility='menu_full',
 111                scale_origin_stack_offset=scale_origin,
 112                stack_offset=(
 113                    (0, -15)
 114                    if uiscale is bui.UIScale.SMALL
 115                    else (0, 0)
 116                    if uiscale is bui.UIScale.MEDIUM
 117                    else (0, 0)
 118                ),
 119                transition=transition,
 120                scale=(
 121                    1.2
 122                    if uiscale is bui.UIScale.SMALL
 123                    else 0.8
 124                    if uiscale is bui.UIScale.MEDIUM
 125                    else 0.75
 126                ),
 127            )
 128        )
 129
 130        if app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL:
 131            self._back_button = None
 132        else:
 133            self._back_button = bui.buttonwidget(
 134                parent=self._root_widget,
 135                position=(
 136                    75 + x_inset,
 137                    self._height
 138                    - 87
 139                    - (4 if uiscale is bui.UIScale.SMALL else 0),
 140                ),
 141                size=(120, 60),
 142                scale=1.2,
 143                autoselect=True,
 144                label=bui.Lstr(resource='backText'),
 145                button_type='back',
 146            )
 147
 148        self._league_rank_button: LeagueRankButton | None
 149        self._store_button: StoreButton | None
 150        self._store_button_widget: bui.Widget | None
 151        self._league_rank_button_widget: bui.Widget | None
 152
 153        if not app.ui_v1.use_toolbars:
 154            prb = self._league_rank_button = LeagueRankButton(
 155                parent=self._root_widget,
 156                position=(
 157                    self._width - (282 + x_inset),
 158                    self._height
 159                    - 85
 160                    - (4 if uiscale is bui.UIScale.SMALL else 0),
 161                ),
 162                size=(100, 60),
 163                color=(0.4, 0.4, 0.9),
 164                textcolor=(0.9, 0.9, 2.0),
 165                scale=1.05,
 166                on_activate_call=bui.WeakCall(self._switch_to_league_rankings),
 167            )
 168            self._league_rank_button_widget = prb.get_button()
 169
 170            sbtn = self._store_button = StoreButton(
 171                parent=self._root_widget,
 172                position=(
 173                    self._width - (170 + x_inset),
 174                    self._height
 175                    - 85
 176                    - (4 if uiscale is bui.UIScale.SMALL else 0),
 177                ),
 178                size=(100, 60),
 179                color=(0.6, 0.4, 0.7),
 180                show_tickets=True,
 181                button_type='square',
 182                sale_scale=0.85,
 183                textcolor=(0.9, 0.7, 1.0),
 184                scale=1.05,
 185                on_activate_call=bui.WeakCall(self._switch_to_score, None),
 186            )
 187            self._store_button_widget = sbtn.get_button()
 188            bui.widget(
 189                edit=self._back_button,
 190                right_widget=self._league_rank_button_widget,
 191            )
 192            bui.widget(
 193                edit=self._league_rank_button_widget,
 194                left_widget=self._back_button,
 195            )
 196        else:
 197            self._league_rank_button = None
 198            self._store_button = None
 199            self._store_button_widget = None
 200            self._league_rank_button_widget = None
 201
 202        # Move our corner buttons dynamically to keep them out of the way of
 203        # the party icon :-(
 204        self._update_corner_button_positions()
 205        self._update_corner_button_positions_timer = bui.AppTimer(
 206            1.0, bui.WeakCall(self._update_corner_button_positions), repeat=True
 207        )
 208
 209        self._last_tournament_query_time: float | None = None
 210        self._last_tournament_query_response_time: float | None = None
 211        self._doing_tournament_query = False
 212
 213        self._selected_campaign_level = cfg.get(
 214            'Selected Coop Campaign Level', None
 215        )
 216        self._selected_custom_level = cfg.get(
 217            'Selected Coop Custom Level', None
 218        )
 219
 220        # Don't want initial construction affecting our last-selected.
 221        self._do_selection_callbacks = False
 222        v = self._height - 95
 223        txt = bui.textwidget(
 224            parent=self._root_widget,
 225            position=(
 226                self._width * 0.5,
 227                v + 40 - (0 if uiscale is bui.UIScale.SMALL else 0),
 228            ),
 229            size=(0, 0),
 230            text=bui.Lstr(
 231                resource='playModes.singlePlayerCoopText',
 232                fallback_resource='playModes.coopText',
 233            ),
 234            h_align='center',
 235            color=app.ui_v1.title_color,
 236            scale=1.5,
 237            maxwidth=500,
 238            v_align='center',
 239        )
 240
 241        if app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL:
 242            bui.textwidget(edit=txt, text='')
 243
 244        if self._back_button is not None:
 245            bui.buttonwidget(
 246                edit=self._back_button,
 247                button_type='backSmall',
 248                size=(60, 50),
 249                position=(
 250                    75 + x_inset,
 251                    self._height
 252                    - 87
 253                    - (4 if uiscale is bui.UIScale.SMALL else 0)
 254                    + 6,
 255                ),
 256                label=bui.charstr(bui.SpecialChar.BACK),
 257            )
 258
 259        self._selected_row = cfg.get('Selected Coop Row', None)
 260
 261        self._scroll_width = self._width - (130 + 2 * x_inset)
 262        self._scroll_height = self._height - (
 263            190
 264            if uiscale is bui.UIScale.SMALL and app.ui_v1.use_toolbars
 265            else 160
 266        )
 267
 268        self._subcontainerwidth = 800.0
 269        self._subcontainerheight = 1400.0
 270
 271        self._scrollwidget = bui.scrollwidget(
 272            parent=self._root_widget,
 273            highlight=False,
 274            position=(65 + x_inset, 120)
 275            if uiscale is bui.UIScale.SMALL and app.ui_v1.use_toolbars
 276            else (65 + x_inset, 70),
 277            size=(self._scroll_width, self._scroll_height),
 278            simple_culling_v=10.0,
 279            claims_left_right=True,
 280            claims_tab=True,
 281            selection_loops_to_parent=True,
 282        )
 283        self._subcontainer: bui.Widget | None = None
 284
 285        # Take note of our account state; we'll refresh later if this changes.
 286        self._account_state_num = plus.get_v1_account_state_num()
 287
 288        # Same for fg/bg state.
 289        self._fg_state = app.fg_state
 290
 291        self._refresh()
 292        self._restore_state()
 293
 294        # Even though we might display cached tournament data immediately, we
 295        # don't consider it valid until we've pinged.
 296        # the server for an update
 297        self._tourney_data_up_to_date = False
 298
 299        # If we've got a cached tournament list for our account and info for
 300        # each one of those tournaments, go ahead and display it as a
 301        # starting point.
 302        if (
 303            app.classic.accounts.account_tournament_list is not None
 304            and app.classic.accounts.account_tournament_list[0]
 305            == plus.get_v1_account_state_num()
 306            and all(
 307                t_id in app.classic.accounts.tournament_info
 308                for t_id in app.classic.accounts.account_tournament_list[1]
 309            )
 310        ):
 311            tourney_data = [
 312                app.classic.accounts.tournament_info[t_id]
 313                for t_id in app.classic.accounts.account_tournament_list[1]
 314            ]
 315            self._update_for_data(tourney_data)
 316
 317        # This will pull new data periodically, update timers, etc.
 318        self._update_timer = bui.AppTimer(
 319            1.0, bui.WeakCall(self._update), repeat=True
 320        )
 321        self._update()
 322
 323    def _update_corner_button_positions(self) -> None:
 324        assert bui.app.classic is not None
 325        uiscale = bui.app.ui_v1.uiscale
 326        offs = (
 327            -55
 328            if uiscale is bui.UIScale.SMALL and bui.is_party_icon_visible()
 329            else 0
 330        )
 331        if self._league_rank_button is not None:
 332            self._league_rank_button.set_position(
 333                (
 334                    self._width - 282 + offs - self._x_inset,
 335                    self._height
 336                    - 85
 337                    - (4 if uiscale is bui.UIScale.SMALL else 0),
 338                )
 339            )
 340        if self._store_button is not None:
 341            self._store_button.set_position(
 342                (
 343                    self._width - 170 + offs - self._x_inset,
 344                    self._height
 345                    - 85
 346                    - (4 if uiscale is bui.UIScale.SMALL else 0),
 347                )
 348            )
 349
 350    # noinspection PyUnresolvedReferences
 351    @staticmethod
 352    def _preload_modules() -> None:
 353        """Preload modules we use; avoids hitches (called in bg thread)."""
 354        import bauiv1lib.purchase as _unused1
 355        import bauiv1lib.coop.gamebutton as _unused2
 356        import bauiv1lib.confirm as _unused3
 357        import bauiv1lib.account as _unused4
 358        import bauiv1lib.league.rankwindow as _unused5
 359        import bauiv1lib.store.browser as _unused6
 360        import bauiv1lib.account.viewer as _unused7
 361        import bauiv1lib.tournamentscores as _unused8
 362        import bauiv1lib.tournamententry as _unused9
 363        import bauiv1lib.play as _unused10
 364        import bauiv1lib.coop.tournamentbutton as _unused11
 365
 366    def _update(self) -> None:
 367        plus = bui.app.plus
 368        assert plus is not None
 369
 370        # Do nothing if we've somehow outlived our actual UI.
 371        if not self._root_widget:
 372            return
 373
 374        cur_time = bui.apptime()
 375
 376        # If its been a while since we got a tournament update, consider the
 377        # data invalid (prevents us from joining tournaments if our internet
 378        # connection goes down for a while).
 379        if (
 380            self._last_tournament_query_response_time is None
 381            or bui.apptime() - self._last_tournament_query_response_time
 382            > 60.0 * 2
 383        ):
 384            self._tourney_data_up_to_date = False
 385
 386        # If our account state has changed, do a full request.
 387        account_state_num = plus.get_v1_account_state_num()
 388        if account_state_num != self._account_state_num:
 389            self._account_state_num = account_state_num
 390            self._save_state()
 391            self._refresh()
 392
 393            # Also encourage a new tournament query since this will clear out
 394            # our current results.
 395            if not self._doing_tournament_query:
 396                self._last_tournament_query_time = None
 397
 398        # If we've been backgrounded/foregrounded, invalidate our
 399        # tournament entries (they will be refreshed below asap).
 400        if self._fg_state != bui.app.fg_state:
 401            self._tourney_data_up_to_date = False
 402
 403        # Send off a new tournament query if its been long enough or whatnot.
 404        if not self._doing_tournament_query and (
 405            self._last_tournament_query_time is None
 406            or cur_time - self._last_tournament_query_time > 30.0
 407            or self._fg_state != bui.app.fg_state
 408        ):
 409            self._fg_state = bui.app.fg_state
 410            self._last_tournament_query_time = cur_time
 411            self._doing_tournament_query = True
 412            plus.tournament_query(
 413                args={'source': 'coop window refresh', 'numScores': 1},
 414                callback=bui.WeakCall(self._on_tournament_query_response),
 415            )
 416
 417        # Decrement time on our tournament buttons.
 418        ads_enabled = bui.have_incentivized_ad()
 419        for tbtn in self._tournament_buttons:
 420            tbtn.time_remaining = max(0, tbtn.time_remaining - 1)
 421            if tbtn.time_remaining_value_text is not None:
 422                bui.textwidget(
 423                    edit=tbtn.time_remaining_value_text,
 424                    text=bui.timestring(tbtn.time_remaining, centi=False)
 425                    if (
 426                        tbtn.has_time_remaining
 427                        and self._tourney_data_up_to_date
 428                    )
 429                    else '-',
 430                )
 431
 432            # Also adjust the ad icon visibility.
 433            if tbtn.allow_ads and bui.has_video_ads():
 434                bui.imagewidget(
 435                    edit=tbtn.entry_fee_ad_image,
 436                    opacity=1.0 if ads_enabled else 0.25,
 437                )
 438                bui.textwidget(
 439                    edit=tbtn.entry_fee_text_remaining,
 440                    color=(0.6, 0.6, 0.6, 1 if ads_enabled else 0.2),
 441                )
 442
 443        self._update_hard_mode_lock_image()
 444
 445    def _update_hard_mode_lock_image(self) -> None:
 446        assert bui.app.classic is not None
 447        try:
 448            bui.imagewidget(
 449                edit=self._hard_button_lock_image,
 450                opacity=0.0
 451                if bui.app.classic.accounts.have_pro_options()
 452                else 1.0,
 453            )
 454        except Exception:
 455            logging.exception('Error updating campaign lock.')
 456
 457    def _update_for_data(self, data: list[dict[str, Any]] | None) -> None:
 458        # If the number of tournaments or challenges in the data differs from
 459        # our current arrangement, refresh with the new number.
 460        if (data is None and self._tournament_button_count != 0) or (
 461            data is not None and (len(data) != self._tournament_button_count)
 462        ):
 463            self._tournament_button_count = len(data) if data is not None else 0
 464            bui.app.config['Tournament Rows'] = self._tournament_button_count
 465            self._refresh()
 466
 467        # Update all of our tourney buttons based on whats in data.
 468        for i, tbtn in enumerate(self._tournament_buttons):
 469            assert data is not None
 470            tbtn.update_for_data(data[i])
 471
 472    def _on_tournament_query_response(
 473        self, data: dict[str, Any] | None
 474    ) -> None:
 475        plus = bui.app.plus
 476        assert plus is not None
 477
 478        assert bui.app.classic is not None
 479        accounts = bui.app.classic.accounts
 480        if data is not None:
 481            tournament_data = data['t']  # This used to be the whole payload.
 482            self._last_tournament_query_response_time = bui.apptime()
 483        else:
 484            tournament_data = None
 485
 486        # Keep our cached tourney info up to date.
 487        if data is not None:
 488            self._tourney_data_up_to_date = True
 489            accounts.cache_tournament_info(tournament_data)
 490
 491            # Also cache the current tourney list/order for this account.
 492            accounts.account_tournament_list = (
 493                plus.get_v1_account_state_num(),
 494                [e['tournamentID'] for e in tournament_data],
 495            )
 496
 497        self._doing_tournament_query = False
 498        self._update_for_data(tournament_data)
 499
 500    def _set_campaign_difficulty(self, difficulty: str) -> None:
 501        # pylint: disable=cyclic-import
 502        from bauiv1lib.purchase import PurchaseWindow
 503
 504        plus = bui.app.plus
 505        assert plus is not None
 506
 507        assert bui.app.classic is not None
 508        if difficulty != self._campaign_difficulty:
 509            if (
 510                difficulty == 'hard'
 511                and not bui.app.classic.accounts.have_pro_options()
 512            ):
 513                PurchaseWindow(items=['pro'])
 514                return
 515            bui.getsound('gunCocking').play()
 516            if difficulty not in ('easy', 'hard'):
 517                print('ERROR: invalid campaign difficulty:', difficulty)
 518                difficulty = 'easy'
 519            self._campaign_difficulty = difficulty
 520            plus.add_v1_account_transaction(
 521                {
 522                    'type': 'SET_MISC_VAL',
 523                    'name': 'campaignDifficulty',
 524                    'value': difficulty,
 525                }
 526            )
 527            self._refresh_campaign_row()
 528        else:
 529            bui.getsound('click01').play()
 530
 531    def _refresh_campaign_row(self) -> None:
 532        # pylint: disable=too-many-locals
 533        # pylint: disable=cyclic-import
 534        from bauiv1lib.coop.gamebutton import GameButton
 535
 536        parent_widget = self._campaign_sub_container
 537
 538        # Clear out anything in the parent widget already.
 539        assert parent_widget is not None
 540        for child in parent_widget.get_children():
 541            child.delete()
 542
 543        next_widget_down = self._tournament_info_button
 544
 545        h = 0
 546        v2 = -2
 547        sel_color = (0.75, 0.85, 0.5)
 548        sel_color_hard = (0.4, 0.7, 0.2)
 549        un_sel_color = (0.5, 0.5, 0.5)
 550        sel_textcolor = (2, 2, 0.8)
 551        un_sel_textcolor = (0.6, 0.6, 0.6)
 552        self._easy_button = bui.buttonwidget(
 553            parent=parent_widget,
 554            position=(h + 30, v2 + 105),
 555            size=(120, 70),
 556            label=bui.Lstr(resource='difficultyEasyText'),
 557            button_type='square',
 558            autoselect=True,
 559            enable_sound=False,
 560            on_activate_call=bui.Call(self._set_campaign_difficulty, 'easy'),
 561            on_select_call=bui.Call(self.sel_change, 'campaign', 'easyButton'),
 562            color=sel_color
 563            if self._campaign_difficulty == 'easy'
 564            else un_sel_color,
 565            textcolor=sel_textcolor
 566            if self._campaign_difficulty == 'easy'
 567            else un_sel_textcolor,
 568        )
 569        bui.widget(edit=self._easy_button, show_buffer_left=100)
 570        if self._selected_campaign_level == 'easyButton':
 571            bui.containerwidget(
 572                edit=parent_widget,
 573                selected_child=self._easy_button,
 574                visible_child=self._easy_button,
 575            )
 576        lock_tex = bui.gettexture('lock')
 577
 578        self._hard_button = bui.buttonwidget(
 579            parent=parent_widget,
 580            position=(h + 30, v2 + 32),
 581            size=(120, 70),
 582            label=bui.Lstr(resource='difficultyHardText'),
 583            button_type='square',
 584            autoselect=True,
 585            enable_sound=False,
 586            on_activate_call=bui.Call(self._set_campaign_difficulty, 'hard'),
 587            on_select_call=bui.Call(self.sel_change, 'campaign', 'hardButton'),
 588            color=sel_color_hard
 589            if self._campaign_difficulty == 'hard'
 590            else un_sel_color,
 591            textcolor=sel_textcolor
 592            if self._campaign_difficulty == 'hard'
 593            else un_sel_textcolor,
 594        )
 595        self._hard_button_lock_image = bui.imagewidget(
 596            parent=parent_widget,
 597            size=(30, 30),
 598            draw_controller=self._hard_button,
 599            position=(h + 30 - 10, v2 + 32 + 70 - 35),
 600            texture=lock_tex,
 601        )
 602        self._update_hard_mode_lock_image()
 603        bui.widget(edit=self._hard_button, show_buffer_left=100)
 604        if self._selected_campaign_level == 'hardButton':
 605            bui.containerwidget(
 606                edit=parent_widget,
 607                selected_child=self._hard_button,
 608                visible_child=self._hard_button,
 609            )
 610
 611        bui.widget(edit=self._hard_button, down_widget=next_widget_down)
 612        h_spacing = 200
 613        campaign_buttons = []
 614        if self._campaign_difficulty == 'easy':
 615            campaignname = 'Easy'
 616        else:
 617            campaignname = 'Default'
 618        items = [
 619            campaignname + ':Onslaught Training',
 620            campaignname + ':Rookie Onslaught',
 621            campaignname + ':Rookie Football',
 622            campaignname + ':Pro Onslaught',
 623            campaignname + ':Pro Football',
 624            campaignname + ':Pro Runaround',
 625            campaignname + ':Uber Onslaught',
 626            campaignname + ':Uber Football',
 627            campaignname + ':Uber Runaround',
 628        ]
 629        items += [campaignname + ':The Last Stand']
 630        if self._selected_campaign_level is None:
 631            self._selected_campaign_level = items[0]
 632        h = 150
 633        for i in items:
 634            is_last_sel = i == self._selected_campaign_level
 635            campaign_buttons.append(
 636                GameButton(
 637                    self, parent_widget, i, h, v2, is_last_sel, 'campaign'
 638                ).get_button()
 639            )
 640            h += h_spacing
 641
 642        bui.widget(edit=campaign_buttons[0], left_widget=self._easy_button)
 643
 644        if self._back_button is not None:
 645            bui.widget(edit=self._easy_button, up_widget=self._back_button)
 646            for btn in campaign_buttons:
 647                bui.widget(
 648                    edit=btn,
 649                    up_widget=self._back_button,
 650                    down_widget=next_widget_down,
 651                )
 652
 653        # Update our existing percent-complete text.
 654        assert bui.app.classic is not None
 655        campaign = bui.app.classic.getcampaign(campaignname)
 656        levels = campaign.levels
 657        levels_complete = sum((1 if l.complete else 0) for l in levels)
 658
 659        # Last level cant be completed; hence the -1.
 660        progress = min(1.0, float(levels_complete) / (len(levels) - 1))
 661        p_str = str(int(progress * 100.0)) + '%'
 662
 663        self._campaign_percent_text = bui.textwidget(
 664            edit=self._campaign_percent_text,
 665            text=bui.Lstr(
 666                value='${C} (${P})',
 667                subs=[
 668                    ('${C}', bui.Lstr(resource=self._r + '.campaignText')),
 669                    ('${P}', p_str),
 670                ],
 671            ),
 672        )
 673
 674    def _on_tournament_info_press(self) -> None:
 675        # pylint: disable=cyclic-import
 676        from bauiv1lib.confirm import ConfirmWindow
 677
 678        txt = bui.Lstr(resource=self._r + '.tournamentInfoText')
 679        ConfirmWindow(
 680            txt,
 681            cancel_button=False,
 682            width=550,
 683            height=260,
 684            origin_widget=self._tournament_info_button,
 685        )
 686
 687    def _refresh(self) -> None:
 688        # pylint: disable=too-many-statements
 689        # pylint: disable=too-many-branches
 690        # pylint: disable=too-many-locals
 691        # pylint: disable=cyclic-import
 692        from bauiv1lib.coop.gamebutton import GameButton
 693        from bauiv1lib.coop.tournamentbutton import TournamentButton
 694
 695        plus = bui.app.plus
 696        assert plus is not None
 697        assert bui.app.classic is not None
 698
 699        # (Re)create the sub-container if need be.
 700        if self._subcontainer is not None:
 701            self._subcontainer.delete()
 702
 703        tourney_row_height = 200
 704        self._subcontainerheight = (
 705            620 + self._tournament_button_count * tourney_row_height
 706        )
 707
 708        self._subcontainer = bui.containerwidget(
 709            parent=self._scrollwidget,
 710            size=(self._subcontainerwidth, self._subcontainerheight),
 711            background=False,
 712            claims_left_right=True,
 713            claims_tab=True,
 714            selection_loops_to_parent=True,
 715        )
 716
 717        bui.containerwidget(
 718            edit=self._root_widget, selected_child=self._scrollwidget
 719        )
 720        if self._back_button is not None:
 721            bui.containerwidget(
 722                edit=self._root_widget, cancel_button=self._back_button
 723            )
 724
 725        w_parent = self._subcontainer
 726        h_base = 6
 727
 728        v = self._subcontainerheight - 73
 729
 730        self._campaign_percent_text = bui.textwidget(
 731            parent=w_parent,
 732            position=(h_base + 27, v + 30),
 733            size=(0, 0),
 734            text='',
 735            h_align='left',
 736            v_align='center',
 737            color=bui.app.ui_v1.title_color,
 738            scale=1.1,
 739        )
 740
 741        row_v_show_buffer = 100
 742        v -= 198
 743
 744        h_scroll = bui.hscrollwidget(
 745            parent=w_parent,
 746            size=(self._scroll_width - 10, 205),
 747            position=(-5, v),
 748            simple_culling_h=70,
 749            highlight=False,
 750            border_opacity=0.0,
 751            color=(0.45, 0.4, 0.5),
 752            on_select_call=lambda: self._on_row_selected('campaign'),
 753        )
 754        self._campaign_h_scroll = h_scroll
 755        bui.widget(
 756            edit=h_scroll,
 757            show_buffer_top=row_v_show_buffer,
 758            show_buffer_bottom=row_v_show_buffer,
 759            autoselect=True,
 760        )
 761        if self._selected_row == 'campaign':
 762            bui.containerwidget(
 763                edit=w_parent, selected_child=h_scroll, visible_child=h_scroll
 764            )
 765        bui.containerwidget(edit=h_scroll, claims_left_right=True)
 766        self._campaign_sub_container = bui.containerwidget(
 767            parent=h_scroll, size=(180 + 200 * 10, 200), background=False
 768        )
 769
 770        # Tournaments
 771
 772        self._tournament_buttons: list[TournamentButton] = []
 773
 774        v -= 53
 775        # FIXME shouldn't use hard-coded strings here.
 776        txt = bui.Lstr(
 777            resource='tournamentsText', fallback_resource='tournamentText'
 778        ).evaluate()
 779        t_width = bui.get_string_width(txt, suppress_warning=True)
 780        bui.textwidget(
 781            parent=w_parent,
 782            position=(h_base + 27, v + 30),
 783            size=(0, 0),
 784            text=txt,
 785            h_align='left',
 786            v_align='center',
 787            color=bui.app.ui_v1.title_color,
 788            scale=1.1,
 789        )
 790        self._tournament_info_button = bui.buttonwidget(
 791            parent=w_parent,
 792            label='?',
 793            size=(20, 20),
 794            text_scale=0.6,
 795            position=(h_base + 27 + t_width * 1.1 + 15, v + 18),
 796            button_type='square',
 797            color=(0.6, 0.5, 0.65),
 798            textcolor=(0.7, 0.6, 0.75),
 799            autoselect=True,
 800            up_widget=self._campaign_h_scroll,
 801            on_activate_call=self._on_tournament_info_press,
 802        )
 803        bui.widget(
 804            edit=self._tournament_info_button,
 805            left_widget=self._tournament_info_button,
 806            right_widget=self._tournament_info_button,
 807        )
 808
 809        # Say 'unavailable' if there are zero tournaments, and if we're not
 810        # signed in add that as well (that's probably why we see
 811        # no tournaments).
 812        if self._tournament_button_count == 0:
 813            unavailable_text = bui.Lstr(resource='unavailableText')
 814            if plus.get_v1_account_state() != 'signed_in':
 815                unavailable_text = bui.Lstr(
 816                    value='${A} (${B})',
 817                    subs=[
 818                        ('${A}', unavailable_text),
 819                        ('${B}', bui.Lstr(resource='notSignedInText')),
 820                    ],
 821                )
 822            bui.textwidget(
 823                parent=w_parent,
 824                position=(h_base + 47, v),
 825                size=(0, 0),
 826                text=unavailable_text,
 827                h_align='left',
 828                v_align='center',
 829                color=bui.app.ui_v1.title_color,
 830                scale=0.9,
 831            )
 832            v -= 40
 833        v -= 198
 834
 835        tournament_h_scroll = None
 836        if self._tournament_button_count > 0:
 837            for i in range(self._tournament_button_count):
 838                tournament_h_scroll = h_scroll = bui.hscrollwidget(
 839                    parent=w_parent,
 840                    size=(self._scroll_width - 10, 205),
 841                    position=(-5, v),
 842                    highlight=False,
 843                    border_opacity=0.0,
 844                    color=(0.45, 0.4, 0.5),
 845                    on_select_call=bui.Call(
 846                        self._on_row_selected, 'tournament' + str(i + 1)
 847                    ),
 848                )
 849                bui.widget(
 850                    edit=h_scroll,
 851                    show_buffer_top=row_v_show_buffer,
 852                    show_buffer_bottom=row_v_show_buffer,
 853                    autoselect=True,
 854                )
 855                if self._selected_row == 'tournament' + str(i + 1):
 856                    bui.containerwidget(
 857                        edit=w_parent,
 858                        selected_child=h_scroll,
 859                        visible_child=h_scroll,
 860                    )
 861                bui.containerwidget(edit=h_scroll, claims_left_right=True)
 862                sc2 = bui.containerwidget(
 863                    parent=h_scroll,
 864                    size=(self._scroll_width - 24, 200),
 865                    background=False,
 866                )
 867                h = 0
 868                v2 = -2
 869                is_last_sel = True
 870                self._tournament_buttons.append(
 871                    TournamentButton(
 872                        sc2,
 873                        h,
 874                        v2,
 875                        is_last_sel,
 876                        on_pressed=bui.WeakCall(self.run_tournament),
 877                    )
 878                )
 879                v -= 200
 880
 881        # Custom Games. (called 'Practice' in UI these days).
 882        v -= 50
 883        bui.textwidget(
 884            parent=w_parent,
 885            position=(h_base + 27, v + 30 + 198),
 886            size=(0, 0),
 887            text=bui.Lstr(
 888                resource='practiceText',
 889                fallback_resource='coopSelectWindow.customText',
 890            ),
 891            h_align='left',
 892            v_align='center',
 893            color=bui.app.ui_v1.title_color,
 894            scale=1.1,
 895        )
 896
 897        items = [
 898            'Challenges:Infinite Onslaught',
 899            'Challenges:Infinite Runaround',
 900            'Challenges:Ninja Fight',
 901            'Challenges:Pro Ninja Fight',
 902            'Challenges:Meteor Shower',
 903            'Challenges:Target Practice B',
 904            'Challenges:Target Practice',
 905        ]
 906
 907        # Show easter-egg-hunt either if its easter or we own it.
 908        if plus.get_v1_account_misc_read_val(
 909            'easter', False
 910        ) or plus.get_purchased('games.easter_egg_hunt'):
 911            items = [
 912                'Challenges:Easter Egg Hunt',
 913                'Challenges:Pro Easter Egg Hunt',
 914            ] + items
 915
 916        # If we've defined custom games, put them at the beginning.
 917        if bui.app.classic.custom_coop_practice_games:
 918            items = bui.app.classic.custom_coop_practice_games + items
 919
 920        self._custom_h_scroll = custom_h_scroll = h_scroll = bui.hscrollwidget(
 921            parent=w_parent,
 922            size=(self._scroll_width - 10, 205),
 923            position=(-5, v),
 924            highlight=False,
 925            border_opacity=0.0,
 926            color=(0.45, 0.4, 0.5),
 927            on_select_call=bui.Call(self._on_row_selected, 'custom'),
 928        )
 929        bui.widget(
 930            edit=h_scroll,
 931            show_buffer_top=row_v_show_buffer,
 932            show_buffer_bottom=1.5 * row_v_show_buffer,
 933            autoselect=True,
 934        )
 935        if self._selected_row == 'custom':
 936            bui.containerwidget(
 937                edit=w_parent, selected_child=h_scroll, visible_child=h_scroll
 938            )
 939        bui.containerwidget(edit=h_scroll, claims_left_right=True)
 940        sc2 = bui.containerwidget(
 941            parent=h_scroll,
 942            size=(max(self._scroll_width - 24, 30 + 200 * len(items)), 200),
 943            background=False,
 944        )
 945        h_spacing = 200
 946        self._custom_buttons: list[GameButton] = []
 947        h = 0
 948        v2 = -2
 949        for item in items:
 950            is_last_sel = item == self._selected_custom_level
 951            self._custom_buttons.append(
 952                GameButton(self, sc2, item, h, v2, is_last_sel, 'custom')
 953            )
 954            h += h_spacing
 955
 956        # We can't fill in our campaign row until tourney buttons are in place.
 957        # (for wiring up)
 958        self._refresh_campaign_row()
 959
 960        for i, tbutton in enumerate(self._tournament_buttons):
 961            bui.widget(
 962                edit=tbutton.button,
 963                up_widget=self._tournament_info_button
 964                if i == 0
 965                else self._tournament_buttons[i - 1].button,
 966                down_widget=self._tournament_buttons[(i + 1)].button
 967                if i + 1 < len(self._tournament_buttons)
 968                else custom_h_scroll,
 969            )
 970            bui.widget(
 971                edit=tbutton.more_scores_button,
 972                down_widget=self._tournament_buttons[
 973                    (i + 1)
 974                ].current_leader_name_text
 975                if i + 1 < len(self._tournament_buttons)
 976                else custom_h_scroll,
 977            )
 978            bui.widget(
 979                edit=tbutton.current_leader_name_text,
 980                up_widget=self._tournament_info_button
 981                if i == 0
 982                else self._tournament_buttons[i - 1].more_scores_button,
 983            )
 984
 985        for btn in self._custom_buttons:
 986            try:
 987                bui.widget(
 988                    edit=btn.get_button(),
 989                    up_widget=tournament_h_scroll
 990                    if self._tournament_buttons
 991                    else self._tournament_info_button,
 992                )
 993            except Exception:
 994                logging.exception('Error wiring up custom buttons.')
 995
 996        if self._back_button is not None:
 997            bui.buttonwidget(
 998                edit=self._back_button, on_activate_call=self._back
 999            )
1000        else:
1001            bui.containerwidget(
1002                edit=self._root_widget, on_cancel_call=self._back
1003            )
1004
1005        # There's probably several 'onSelected' callbacks pushed onto the
1006        # event queue.. we need to push ours too so we're enabled *after* them.
1007        bui.pushcall(self._enable_selectable_callback)
1008
1009    def _on_row_selected(self, row: str) -> None:
1010        if self._do_selection_callbacks:
1011            if self._selected_row != row:
1012                self._selected_row = row
1013
1014    def _enable_selectable_callback(self) -> None:
1015        self._do_selection_callbacks = True
1016
1017    def _switch_to_league_rankings(self) -> None:
1018        # pylint: disable=cyclic-import
1019        from bauiv1lib.account import show_sign_in_prompt
1020        from bauiv1lib.league.rankwindow import LeagueRankWindow
1021
1022        plus = bui.app.plus
1023        assert plus is not None
1024
1025        if plus.get_v1_account_state() != 'signed_in':
1026            show_sign_in_prompt()
1027            return
1028        self._save_state()
1029        bui.containerwidget(edit=self._root_widget, transition='out_left')
1030        assert self._league_rank_button is not None
1031        assert bui.app.classic is not None
1032        bui.app.ui_v1.set_main_menu_window(
1033            LeagueRankWindow(
1034                origin_widget=self._league_rank_button.get_button()
1035            ).get_root_widget()
1036        )
1037
1038    def _switch_to_score(
1039        self,
1040        show_tab: StoreBrowserWindow.TabID
1041        | None = StoreBrowserWindow.TabID.EXTRAS,
1042    ) -> None:
1043        # pylint: disable=cyclic-import
1044        from bauiv1lib.account import show_sign_in_prompt
1045
1046        plus = bui.app.plus
1047        assert plus is not None
1048
1049        if plus.get_v1_account_state() != 'signed_in':
1050            show_sign_in_prompt()
1051            return
1052        self._save_state()
1053        bui.containerwidget(edit=self._root_widget, transition='out_left')
1054        assert self._store_button is not None
1055        assert bui.app.classic is not None
1056        bui.app.ui_v1.set_main_menu_window(
1057            StoreBrowserWindow(
1058                origin_widget=self._store_button.get_button(),
1059                show_tab=show_tab,
1060                back_location='CoopBrowserWindow',
1061            ).get_root_widget()
1062        )
1063
1064    def is_tourney_data_up_to_date(self) -> bool:
1065        """Return whether our tourney data is up to date."""
1066        return self._tourney_data_up_to_date
1067
1068    def run_game(self, game: str) -> None:
1069        """Run the provided game."""
1070        # pylint: disable=too-many-branches
1071        # pylint: disable=cyclic-import
1072        from bauiv1lib.confirm import ConfirmWindow
1073        from bauiv1lib.purchase import PurchaseWindow
1074        from bauiv1lib.account import show_sign_in_prompt
1075
1076        plus = bui.app.plus
1077        assert plus is not None
1078
1079        assert bui.app.classic is not None
1080
1081        args: dict[str, Any] = {}
1082
1083        if game == 'Easy:The Last Stand':
1084            ConfirmWindow(
1085                bui.Lstr(
1086                    resource='difficultyHardUnlockOnlyText',
1087                    fallback_resource='difficultyHardOnlyText',
1088                ),
1089                cancel_button=False,
1090                width=460,
1091                height=130,
1092            )
1093            return
1094
1095        # Infinite onslaught/runaround require pro; bring up a store link
1096        # if need be.
1097        if (
1098            game
1099            in (
1100                'Challenges:Infinite Runaround',
1101                'Challenges:Infinite Onslaught',
1102            )
1103            and not bui.app.classic.accounts.have_pro()
1104        ):
1105            if plus.get_v1_account_state() != 'signed_in':
1106                show_sign_in_prompt()
1107            else:
1108                PurchaseWindow(items=['pro'])
1109            return
1110
1111        required_purchase: str | None
1112        if game in ['Challenges:Meteor Shower']:
1113            required_purchase = 'games.meteor_shower'
1114        elif game in [
1115            'Challenges:Target Practice',
1116            'Challenges:Target Practice B',
1117        ]:
1118            required_purchase = 'games.target_practice'
1119        elif game in ['Challenges:Ninja Fight']:
1120            required_purchase = 'games.ninja_fight'
1121        elif game in ['Challenges:Pro Ninja Fight']:
1122            required_purchase = 'games.ninja_fight'
1123        elif game in [
1124            'Challenges:Easter Egg Hunt',
1125            'Challenges:Pro Easter Egg Hunt',
1126        ]:
1127            required_purchase = 'games.easter_egg_hunt'
1128        else:
1129            required_purchase = None
1130
1131        if required_purchase is not None and not plus.get_purchased(
1132            required_purchase
1133        ):
1134            if plus.get_v1_account_state() != 'signed_in':
1135                show_sign_in_prompt()
1136            else:
1137                PurchaseWindow(items=[required_purchase])
1138            return
1139
1140        self._save_state()
1141
1142        if bui.app.classic.launch_coop_game(game, args=args):
1143            bui.containerwidget(edit=self._root_widget, transition='out_left')
1144
1145    def run_tournament(self, tournament_button: TournamentButton) -> None:
1146        """Run the provided tournament game."""
1147        from bauiv1lib.account import show_sign_in_prompt
1148        from bauiv1lib.tournamententry import TournamentEntryWindow
1149
1150        plus = bui.app.plus
1151        assert plus is not None
1152
1153        if plus.get_v1_account_state() != 'signed_in':
1154            show_sign_in_prompt()
1155            return
1156
1157        if bui.workspaces_in_use():
1158            bui.screenmessage(
1159                bui.Lstr(resource='tournamentsDisabledWorkspaceText'),
1160                color=(1, 0, 0),
1161            )
1162            bui.getsound('error').play()
1163            return
1164
1165        if not self._tourney_data_up_to_date:
1166            bui.screenmessage(
1167                bui.Lstr(resource='tournamentCheckingStateText'),
1168                color=(1, 1, 0),
1169            )
1170            bui.getsound('error').play()
1171            return
1172
1173        if tournament_button.tournament_id is None:
1174            bui.screenmessage(
1175                bui.Lstr(resource='internal.unavailableNoConnectionText'),
1176                color=(1, 0, 0),
1177            )
1178            bui.getsound('error').play()
1179            return
1180
1181        if tournament_button.required_league is not None:
1182            bui.screenmessage(
1183                bui.Lstr(
1184                    resource='league.tournamentLeagueText',
1185                    subs=[
1186                        (
1187                            '${NAME}',
1188                            bui.Lstr(
1189                                translate=(
1190                                    'leagueNames',
1191                                    tournament_button.required_league,
1192                                )
1193                            ),
1194                        )
1195                    ],
1196                ),
1197                color=(1, 0, 0),
1198            )
1199            bui.getsound('error').play()
1200            return
1201
1202        if tournament_button.time_remaining <= 0:
1203            bui.screenmessage(
1204                bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0)
1205            )
1206            bui.getsound('error').play()
1207            return
1208
1209        self._save_state()
1210
1211        assert tournament_button.tournament_id is not None
1212        TournamentEntryWindow(
1213            tournament_id=tournament_button.tournament_id,
1214            position=tournament_button.button.get_screen_space_center(),
1215        )
1216
1217    def _back(self) -> None:
1218        # pylint: disable=cyclic-import
1219        from bauiv1lib.play import PlayWindow
1220
1221        # If something is selected, store it.
1222        self._save_state()
1223        bui.containerwidget(
1224            edit=self._root_widget, transition=self._transition_out
1225        )
1226        assert bui.app.classic is not None
1227        bui.app.ui_v1.set_main_menu_window(
1228            PlayWindow(transition='in_left').get_root_widget()
1229        )
1230
1231    def _save_state(self) -> None:
1232        cfg = bui.app.config
1233        try:
1234            sel = self._root_widget.get_selected_child()
1235            if sel == self._back_button:
1236                sel_name = 'Back'
1237            elif sel == self._store_button_widget:
1238                sel_name = 'Store'
1239            elif sel == self._league_rank_button_widget:
1240                sel_name = 'PowerRanking'
1241            elif sel == self._scrollwidget:
1242                sel_name = 'Scroll'
1243            else:
1244                raise ValueError('unrecognized selection')
1245            assert bui.app.classic is not None
1246            bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name}
1247        except Exception:
1248            logging.exception('Error saving state for %s.', self)
1249
1250        cfg['Selected Coop Row'] = self._selected_row
1251        cfg['Selected Coop Custom Level'] = self._selected_custom_level
1252        cfg['Selected Coop Campaign Level'] = self._selected_campaign_level
1253        cfg.commit()
1254
1255    def _restore_state(self) -> None:
1256        try:
1257            assert bui.app.classic is not None
1258            sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get(
1259                'sel_name'
1260            )
1261            if sel_name == 'Back':
1262                sel = self._back_button
1263            elif sel_name == 'Scroll':
1264                sel = self._scrollwidget
1265            elif sel_name == 'PowerRanking':
1266                sel = self._league_rank_button_widget
1267            elif sel_name == 'Store':
1268                sel = self._store_button_widget
1269            else:
1270                sel = self._scrollwidget
1271            bui.containerwidget(edit=self._root_widget, selected_child=sel)
1272        except Exception:
1273            logging.exception('Error restoring state for %s.', self)
1274
1275    def sel_change(self, row: str, game: str) -> None:
1276        """(internal)"""
1277        if self._do_selection_callbacks:
1278            if row == 'custom':
1279                self._selected_custom_level = game
1280            elif row == 'campaign':
1281                self._selected_campaign_level = game
class CoopBrowserWindow(bauiv1._uitypes.Window):
  25class CoopBrowserWindow(bui.Window):
  26    """Window for browsing co-op levels/games/etc."""
  27
  28    def __init__(
  29        self,
  30        transition: str | None = 'in_right',
  31        origin_widget: bui.Widget | None = None,
  32    ):
  33        # pylint: disable=too-many-statements
  34        # pylint: disable=cyclic-import
  35
  36        plus = bui.app.plus
  37        assert plus is not None
  38
  39        # Preload some modules we use in a background thread so we won't
  40        # have a visual hitch when the user taps them.
  41        Thread(target=self._preload_modules).start()
  42
  43        bui.set_analytics_screen('Coop Window')
  44
  45        app = bui.app
  46        assert app.classic is not None
  47        cfg = app.config
  48
  49        # Quick note to players that tourneys won't work in ballistica
  50        # core builds. (need to split the word so it won't get subbed out)
  51        if 'ballistica' + 'kit' == bui.appname():
  52            bui.apptimer(
  53                1.0,
  54                lambda: bui.screenmessage(
  55                    bui.Lstr(resource='noTournamentsInTestBuildText'),
  56                    color=(1, 1, 0),
  57                ),
  58            )
  59
  60        # If they provided an origin-widget, scale up from that.
  61        scale_origin: tuple[float, float] | None
  62        if origin_widget is not None:
  63            self._transition_out = 'out_scale'
  64            scale_origin = origin_widget.get_screen_space_center()
  65            transition = 'in_scale'
  66        else:
  67            self._transition_out = 'out_right'
  68            scale_origin = None
  69
  70        # Try to recreate the same number of buttons we had last time so our
  71        # re-selection code works.
  72        self._tournament_button_count = app.config.get('Tournament Rows', 0)
  73        assert isinstance(self._tournament_button_count, int)
  74
  75        self.star_tex = bui.gettexture('star')
  76        self.lsbt = bui.getmesh('level_select_button_transparent')
  77        self.lsbo = bui.getmesh('level_select_button_opaque')
  78        self.a_outline_tex = bui.gettexture('achievementOutline')
  79        self.a_outline_mesh = bui.getmesh('achievementOutline')
  80        self._campaign_sub_container: bui.Widget | None = None
  81        self._tournament_info_button: bui.Widget | None = None
  82        self._easy_button: bui.Widget | None = None
  83        self._hard_button: bui.Widget | None = None
  84        self._hard_button_lock_image: bui.Widget | None = None
  85        self._campaign_percent_text: bui.Widget | None = None
  86
  87        assert bui.app.classic is not None
  88        uiscale = bui.app.ui_v1.uiscale
  89        self._width = 1320 if uiscale is bui.UIScale.SMALL else 1120
  90        self._x_inset = x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
  91        self._height = (
  92            657
  93            if uiscale is bui.UIScale.SMALL
  94            else 730
  95            if uiscale is bui.UIScale.MEDIUM
  96            else 800
  97        )
  98        app.ui_v1.set_main_menu_location('Coop Select')
  99        self._r = 'coopSelectWindow'
 100        top_extra = 20 if uiscale is bui.UIScale.SMALL else 0
 101
 102        self._tourney_data_up_to_date = False
 103
 104        self._campaign_difficulty = plus.get_v1_account_misc_val(
 105            'campaignDifficulty', 'easy'
 106        )
 107
 108        super().__init__(
 109            root_widget=bui.containerwidget(
 110                size=(self._width, self._height + top_extra),
 111                toolbar_visibility='menu_full',
 112                scale_origin_stack_offset=scale_origin,
 113                stack_offset=(
 114                    (0, -15)
 115                    if uiscale is bui.UIScale.SMALL
 116                    else (0, 0)
 117                    if uiscale is bui.UIScale.MEDIUM
 118                    else (0, 0)
 119                ),
 120                transition=transition,
 121                scale=(
 122                    1.2
 123                    if uiscale is bui.UIScale.SMALL
 124                    else 0.8
 125                    if uiscale is bui.UIScale.MEDIUM
 126                    else 0.75
 127                ),
 128            )
 129        )
 130
 131        if app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL:
 132            self._back_button = None
 133        else:
 134            self._back_button = bui.buttonwidget(
 135                parent=self._root_widget,
 136                position=(
 137                    75 + x_inset,
 138                    self._height
 139                    - 87
 140                    - (4 if uiscale is bui.UIScale.SMALL else 0),
 141                ),
 142                size=(120, 60),
 143                scale=1.2,
 144                autoselect=True,
 145                label=bui.Lstr(resource='backText'),
 146                button_type='back',
 147            )
 148
 149        self._league_rank_button: LeagueRankButton | None
 150        self._store_button: StoreButton | None
 151        self._store_button_widget: bui.Widget | None
 152        self._league_rank_button_widget: bui.Widget | None
 153
 154        if not app.ui_v1.use_toolbars:
 155            prb = self._league_rank_button = LeagueRankButton(
 156                parent=self._root_widget,
 157                position=(
 158                    self._width - (282 + x_inset),
 159                    self._height
 160                    - 85
 161                    - (4 if uiscale is bui.UIScale.SMALL else 0),
 162                ),
 163                size=(100, 60),
 164                color=(0.4, 0.4, 0.9),
 165                textcolor=(0.9, 0.9, 2.0),
 166                scale=1.05,
 167                on_activate_call=bui.WeakCall(self._switch_to_league_rankings),
 168            )
 169            self._league_rank_button_widget = prb.get_button()
 170
 171            sbtn = self._store_button = StoreButton(
 172                parent=self._root_widget,
 173                position=(
 174                    self._width - (170 + x_inset),
 175                    self._height
 176                    - 85
 177                    - (4 if uiscale is bui.UIScale.SMALL else 0),
 178                ),
 179                size=(100, 60),
 180                color=(0.6, 0.4, 0.7),
 181                show_tickets=True,
 182                button_type='square',
 183                sale_scale=0.85,
 184                textcolor=(0.9, 0.7, 1.0),
 185                scale=1.05,
 186                on_activate_call=bui.WeakCall(self._switch_to_score, None),
 187            )
 188            self._store_button_widget = sbtn.get_button()
 189            bui.widget(
 190                edit=self._back_button,
 191                right_widget=self._league_rank_button_widget,
 192            )
 193            bui.widget(
 194                edit=self._league_rank_button_widget,
 195                left_widget=self._back_button,
 196            )
 197        else:
 198            self._league_rank_button = None
 199            self._store_button = None
 200            self._store_button_widget = None
 201            self._league_rank_button_widget = None
 202
 203        # Move our corner buttons dynamically to keep them out of the way of
 204        # the party icon :-(
 205        self._update_corner_button_positions()
 206        self._update_corner_button_positions_timer = bui.AppTimer(
 207            1.0, bui.WeakCall(self._update_corner_button_positions), repeat=True
 208        )
 209
 210        self._last_tournament_query_time: float | None = None
 211        self._last_tournament_query_response_time: float | None = None
 212        self._doing_tournament_query = False
 213
 214        self._selected_campaign_level = cfg.get(
 215            'Selected Coop Campaign Level', None
 216        )
 217        self._selected_custom_level = cfg.get(
 218            'Selected Coop Custom Level', None
 219        )
 220
 221        # Don't want initial construction affecting our last-selected.
 222        self._do_selection_callbacks = False
 223        v = self._height - 95
 224        txt = bui.textwidget(
 225            parent=self._root_widget,
 226            position=(
 227                self._width * 0.5,
 228                v + 40 - (0 if uiscale is bui.UIScale.SMALL else 0),
 229            ),
 230            size=(0, 0),
 231            text=bui.Lstr(
 232                resource='playModes.singlePlayerCoopText',
 233                fallback_resource='playModes.coopText',
 234            ),
 235            h_align='center',
 236            color=app.ui_v1.title_color,
 237            scale=1.5,
 238            maxwidth=500,
 239            v_align='center',
 240        )
 241
 242        if app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL:
 243            bui.textwidget(edit=txt, text='')
 244
 245        if self._back_button is not None:
 246            bui.buttonwidget(
 247                edit=self._back_button,
 248                button_type='backSmall',
 249                size=(60, 50),
 250                position=(
 251                    75 + x_inset,
 252                    self._height
 253                    - 87
 254                    - (4 if uiscale is bui.UIScale.SMALL else 0)
 255                    + 6,
 256                ),
 257                label=bui.charstr(bui.SpecialChar.BACK),
 258            )
 259
 260        self._selected_row = cfg.get('Selected Coop Row', None)
 261
 262        self._scroll_width = self._width - (130 + 2 * x_inset)
 263        self._scroll_height = self._height - (
 264            190
 265            if uiscale is bui.UIScale.SMALL and app.ui_v1.use_toolbars
 266            else 160
 267        )
 268
 269        self._subcontainerwidth = 800.0
 270        self._subcontainerheight = 1400.0
 271
 272        self._scrollwidget = bui.scrollwidget(
 273            parent=self._root_widget,
 274            highlight=False,
 275            position=(65 + x_inset, 120)
 276            if uiscale is bui.UIScale.SMALL and app.ui_v1.use_toolbars
 277            else (65 + x_inset, 70),
 278            size=(self._scroll_width, self._scroll_height),
 279            simple_culling_v=10.0,
 280            claims_left_right=True,
 281            claims_tab=True,
 282            selection_loops_to_parent=True,
 283        )
 284        self._subcontainer: bui.Widget | None = None
 285
 286        # Take note of our account state; we'll refresh later if this changes.
 287        self._account_state_num = plus.get_v1_account_state_num()
 288
 289        # Same for fg/bg state.
 290        self._fg_state = app.fg_state
 291
 292        self._refresh()
 293        self._restore_state()
 294
 295        # Even though we might display cached tournament data immediately, we
 296        # don't consider it valid until we've pinged.
 297        # the server for an update
 298        self._tourney_data_up_to_date = False
 299
 300        # If we've got a cached tournament list for our account and info for
 301        # each one of those tournaments, go ahead and display it as a
 302        # starting point.
 303        if (
 304            app.classic.accounts.account_tournament_list is not None
 305            and app.classic.accounts.account_tournament_list[0]
 306            == plus.get_v1_account_state_num()
 307            and all(
 308                t_id in app.classic.accounts.tournament_info
 309                for t_id in app.classic.accounts.account_tournament_list[1]
 310            )
 311        ):
 312            tourney_data = [
 313                app.classic.accounts.tournament_info[t_id]
 314                for t_id in app.classic.accounts.account_tournament_list[1]
 315            ]
 316            self._update_for_data(tourney_data)
 317
 318        # This will pull new data periodically, update timers, etc.
 319        self._update_timer = bui.AppTimer(
 320            1.0, bui.WeakCall(self._update), repeat=True
 321        )
 322        self._update()
 323
 324    def _update_corner_button_positions(self) -> None:
 325        assert bui.app.classic is not None
 326        uiscale = bui.app.ui_v1.uiscale
 327        offs = (
 328            -55
 329            if uiscale is bui.UIScale.SMALL and bui.is_party_icon_visible()
 330            else 0
 331        )
 332        if self._league_rank_button is not None:
 333            self._league_rank_button.set_position(
 334                (
 335                    self._width - 282 + offs - self._x_inset,
 336                    self._height
 337                    - 85
 338                    - (4 if uiscale is bui.UIScale.SMALL else 0),
 339                )
 340            )
 341        if self._store_button is not None:
 342            self._store_button.set_position(
 343                (
 344                    self._width - 170 + offs - self._x_inset,
 345                    self._height
 346                    - 85
 347                    - (4 if uiscale is bui.UIScale.SMALL else 0),
 348                )
 349            )
 350
 351    # noinspection PyUnresolvedReferences
 352    @staticmethod
 353    def _preload_modules() -> None:
 354        """Preload modules we use; avoids hitches (called in bg thread)."""
 355        import bauiv1lib.purchase as _unused1
 356        import bauiv1lib.coop.gamebutton as _unused2
 357        import bauiv1lib.confirm as _unused3
 358        import bauiv1lib.account as _unused4
 359        import bauiv1lib.league.rankwindow as _unused5
 360        import bauiv1lib.store.browser as _unused6
 361        import bauiv1lib.account.viewer as _unused7
 362        import bauiv1lib.tournamentscores as _unused8
 363        import bauiv1lib.tournamententry as _unused9
 364        import bauiv1lib.play as _unused10
 365        import bauiv1lib.coop.tournamentbutton as _unused11
 366
 367    def _update(self) -> None:
 368        plus = bui.app.plus
 369        assert plus is not None
 370
 371        # Do nothing if we've somehow outlived our actual UI.
 372        if not self._root_widget:
 373            return
 374
 375        cur_time = bui.apptime()
 376
 377        # If its been a while since we got a tournament update, consider the
 378        # data invalid (prevents us from joining tournaments if our internet
 379        # connection goes down for a while).
 380        if (
 381            self._last_tournament_query_response_time is None
 382            or bui.apptime() - self._last_tournament_query_response_time
 383            > 60.0 * 2
 384        ):
 385            self._tourney_data_up_to_date = False
 386
 387        # If our account state has changed, do a full request.
 388        account_state_num = plus.get_v1_account_state_num()
 389        if account_state_num != self._account_state_num:
 390            self._account_state_num = account_state_num
 391            self._save_state()
 392            self._refresh()
 393
 394            # Also encourage a new tournament query since this will clear out
 395            # our current results.
 396            if not self._doing_tournament_query:
 397                self._last_tournament_query_time = None
 398
 399        # If we've been backgrounded/foregrounded, invalidate our
 400        # tournament entries (they will be refreshed below asap).
 401        if self._fg_state != bui.app.fg_state:
 402            self._tourney_data_up_to_date = False
 403
 404        # Send off a new tournament query if its been long enough or whatnot.
 405        if not self._doing_tournament_query and (
 406            self._last_tournament_query_time is None
 407            or cur_time - self._last_tournament_query_time > 30.0
 408            or self._fg_state != bui.app.fg_state
 409        ):
 410            self._fg_state = bui.app.fg_state
 411            self._last_tournament_query_time = cur_time
 412            self._doing_tournament_query = True
 413            plus.tournament_query(
 414                args={'source': 'coop window refresh', 'numScores': 1},
 415                callback=bui.WeakCall(self._on_tournament_query_response),
 416            )
 417
 418        # Decrement time on our tournament buttons.
 419        ads_enabled = bui.have_incentivized_ad()
 420        for tbtn in self._tournament_buttons:
 421            tbtn.time_remaining = max(0, tbtn.time_remaining - 1)
 422            if tbtn.time_remaining_value_text is not None:
 423                bui.textwidget(
 424                    edit=tbtn.time_remaining_value_text,
 425                    text=bui.timestring(tbtn.time_remaining, centi=False)
 426                    if (
 427                        tbtn.has_time_remaining
 428                        and self._tourney_data_up_to_date
 429                    )
 430                    else '-',
 431                )
 432
 433            # Also adjust the ad icon visibility.
 434            if tbtn.allow_ads and bui.has_video_ads():
 435                bui.imagewidget(
 436                    edit=tbtn.entry_fee_ad_image,
 437                    opacity=1.0 if ads_enabled else 0.25,
 438                )
 439                bui.textwidget(
 440                    edit=tbtn.entry_fee_text_remaining,
 441                    color=(0.6, 0.6, 0.6, 1 if ads_enabled else 0.2),
 442                )
 443
 444        self._update_hard_mode_lock_image()
 445
 446    def _update_hard_mode_lock_image(self) -> None:
 447        assert bui.app.classic is not None
 448        try:
 449            bui.imagewidget(
 450                edit=self._hard_button_lock_image,
 451                opacity=0.0
 452                if bui.app.classic.accounts.have_pro_options()
 453                else 1.0,
 454            )
 455        except Exception:
 456            logging.exception('Error updating campaign lock.')
 457
 458    def _update_for_data(self, data: list[dict[str, Any]] | None) -> None:
 459        # If the number of tournaments or challenges in the data differs from
 460        # our current arrangement, refresh with the new number.
 461        if (data is None and self._tournament_button_count != 0) or (
 462            data is not None and (len(data) != self._tournament_button_count)
 463        ):
 464            self._tournament_button_count = len(data) if data is not None else 0
 465            bui.app.config['Tournament Rows'] = self._tournament_button_count
 466            self._refresh()
 467
 468        # Update all of our tourney buttons based on whats in data.
 469        for i, tbtn in enumerate(self._tournament_buttons):
 470            assert data is not None
 471            tbtn.update_for_data(data[i])
 472
 473    def _on_tournament_query_response(
 474        self, data: dict[str, Any] | None
 475    ) -> None:
 476        plus = bui.app.plus
 477        assert plus is not None
 478
 479        assert bui.app.classic is not None
 480        accounts = bui.app.classic.accounts
 481        if data is not None:
 482            tournament_data = data['t']  # This used to be the whole payload.
 483            self._last_tournament_query_response_time = bui.apptime()
 484        else:
 485            tournament_data = None
 486
 487        # Keep our cached tourney info up to date.
 488        if data is not None:
 489            self._tourney_data_up_to_date = True
 490            accounts.cache_tournament_info(tournament_data)
 491
 492            # Also cache the current tourney list/order for this account.
 493            accounts.account_tournament_list = (
 494                plus.get_v1_account_state_num(),
 495                [e['tournamentID'] for e in tournament_data],
 496            )
 497
 498        self._doing_tournament_query = False
 499        self._update_for_data(tournament_data)
 500
 501    def _set_campaign_difficulty(self, difficulty: str) -> None:
 502        # pylint: disable=cyclic-import
 503        from bauiv1lib.purchase import PurchaseWindow
 504
 505        plus = bui.app.plus
 506        assert plus is not None
 507
 508        assert bui.app.classic is not None
 509        if difficulty != self._campaign_difficulty:
 510            if (
 511                difficulty == 'hard'
 512                and not bui.app.classic.accounts.have_pro_options()
 513            ):
 514                PurchaseWindow(items=['pro'])
 515                return
 516            bui.getsound('gunCocking').play()
 517            if difficulty not in ('easy', 'hard'):
 518                print('ERROR: invalid campaign difficulty:', difficulty)
 519                difficulty = 'easy'
 520            self._campaign_difficulty = difficulty
 521            plus.add_v1_account_transaction(
 522                {
 523                    'type': 'SET_MISC_VAL',
 524                    'name': 'campaignDifficulty',
 525                    'value': difficulty,
 526                }
 527            )
 528            self._refresh_campaign_row()
 529        else:
 530            bui.getsound('click01').play()
 531
 532    def _refresh_campaign_row(self) -> None:
 533        # pylint: disable=too-many-locals
 534        # pylint: disable=cyclic-import
 535        from bauiv1lib.coop.gamebutton import GameButton
 536
 537        parent_widget = self._campaign_sub_container
 538
 539        # Clear out anything in the parent widget already.
 540        assert parent_widget is not None
 541        for child in parent_widget.get_children():
 542            child.delete()
 543
 544        next_widget_down = self._tournament_info_button
 545
 546        h = 0
 547        v2 = -2
 548        sel_color = (0.75, 0.85, 0.5)
 549        sel_color_hard = (0.4, 0.7, 0.2)
 550        un_sel_color = (0.5, 0.5, 0.5)
 551        sel_textcolor = (2, 2, 0.8)
 552        un_sel_textcolor = (0.6, 0.6, 0.6)
 553        self._easy_button = bui.buttonwidget(
 554            parent=parent_widget,
 555            position=(h + 30, v2 + 105),
 556            size=(120, 70),
 557            label=bui.Lstr(resource='difficultyEasyText'),
 558            button_type='square',
 559            autoselect=True,
 560            enable_sound=False,
 561            on_activate_call=bui.Call(self._set_campaign_difficulty, 'easy'),
 562            on_select_call=bui.Call(self.sel_change, 'campaign', 'easyButton'),
 563            color=sel_color
 564            if self._campaign_difficulty == 'easy'
 565            else un_sel_color,
 566            textcolor=sel_textcolor
 567            if self._campaign_difficulty == 'easy'
 568            else un_sel_textcolor,
 569        )
 570        bui.widget(edit=self._easy_button, show_buffer_left=100)
 571        if self._selected_campaign_level == 'easyButton':
 572            bui.containerwidget(
 573                edit=parent_widget,
 574                selected_child=self._easy_button,
 575                visible_child=self._easy_button,
 576            )
 577        lock_tex = bui.gettexture('lock')
 578
 579        self._hard_button = bui.buttonwidget(
 580            parent=parent_widget,
 581            position=(h + 30, v2 + 32),
 582            size=(120, 70),
 583            label=bui.Lstr(resource='difficultyHardText'),
 584            button_type='square',
 585            autoselect=True,
 586            enable_sound=False,
 587            on_activate_call=bui.Call(self._set_campaign_difficulty, 'hard'),
 588            on_select_call=bui.Call(self.sel_change, 'campaign', 'hardButton'),
 589            color=sel_color_hard
 590            if self._campaign_difficulty == 'hard'
 591            else un_sel_color,
 592            textcolor=sel_textcolor
 593            if self._campaign_difficulty == 'hard'
 594            else un_sel_textcolor,
 595        )
 596        self._hard_button_lock_image = bui.imagewidget(
 597            parent=parent_widget,
 598            size=(30, 30),
 599            draw_controller=self._hard_button,
 600            position=(h + 30 - 10, v2 + 32 + 70 - 35),
 601            texture=lock_tex,
 602        )
 603        self._update_hard_mode_lock_image()
 604        bui.widget(edit=self._hard_button, show_buffer_left=100)
 605        if self._selected_campaign_level == 'hardButton':
 606            bui.containerwidget(
 607                edit=parent_widget,
 608                selected_child=self._hard_button,
 609                visible_child=self._hard_button,
 610            )
 611
 612        bui.widget(edit=self._hard_button, down_widget=next_widget_down)
 613        h_spacing = 200
 614        campaign_buttons = []
 615        if self._campaign_difficulty == 'easy':
 616            campaignname = 'Easy'
 617        else:
 618            campaignname = 'Default'
 619        items = [
 620            campaignname + ':Onslaught Training',
 621            campaignname + ':Rookie Onslaught',
 622            campaignname + ':Rookie Football',
 623            campaignname + ':Pro Onslaught',
 624            campaignname + ':Pro Football',
 625            campaignname + ':Pro Runaround',
 626            campaignname + ':Uber Onslaught',
 627            campaignname + ':Uber Football',
 628            campaignname + ':Uber Runaround',
 629        ]
 630        items += [campaignname + ':The Last Stand']
 631        if self._selected_campaign_level is None:
 632            self._selected_campaign_level = items[0]
 633        h = 150
 634        for i in items:
 635            is_last_sel = i == self._selected_campaign_level
 636            campaign_buttons.append(
 637                GameButton(
 638                    self, parent_widget, i, h, v2, is_last_sel, 'campaign'
 639                ).get_button()
 640            )
 641            h += h_spacing
 642
 643        bui.widget(edit=campaign_buttons[0], left_widget=self._easy_button)
 644
 645        if self._back_button is not None:
 646            bui.widget(edit=self._easy_button, up_widget=self._back_button)
 647            for btn in campaign_buttons:
 648                bui.widget(
 649                    edit=btn,
 650                    up_widget=self._back_button,
 651                    down_widget=next_widget_down,
 652                )
 653
 654        # Update our existing percent-complete text.
 655        assert bui.app.classic is not None
 656        campaign = bui.app.classic.getcampaign(campaignname)
 657        levels = campaign.levels
 658        levels_complete = sum((1 if l.complete else 0) for l in levels)
 659
 660        # Last level cant be completed; hence the -1.
 661        progress = min(1.0, float(levels_complete) / (len(levels) - 1))
 662        p_str = str(int(progress * 100.0)) + '%'
 663
 664        self._campaign_percent_text = bui.textwidget(
 665            edit=self._campaign_percent_text,
 666            text=bui.Lstr(
 667                value='${C} (${P})',
 668                subs=[
 669                    ('${C}', bui.Lstr(resource=self._r + '.campaignText')),
 670                    ('${P}', p_str),
 671                ],
 672            ),
 673        )
 674
 675    def _on_tournament_info_press(self) -> None:
 676        # pylint: disable=cyclic-import
 677        from bauiv1lib.confirm import ConfirmWindow
 678
 679        txt = bui.Lstr(resource=self._r + '.tournamentInfoText')
 680        ConfirmWindow(
 681            txt,
 682            cancel_button=False,
 683            width=550,
 684            height=260,
 685            origin_widget=self._tournament_info_button,
 686        )
 687
 688    def _refresh(self) -> None:
 689        # pylint: disable=too-many-statements
 690        # pylint: disable=too-many-branches
 691        # pylint: disable=too-many-locals
 692        # pylint: disable=cyclic-import
 693        from bauiv1lib.coop.gamebutton import GameButton
 694        from bauiv1lib.coop.tournamentbutton import TournamentButton
 695
 696        plus = bui.app.plus
 697        assert plus is not None
 698        assert bui.app.classic is not None
 699
 700        # (Re)create the sub-container if need be.
 701        if self._subcontainer is not None:
 702            self._subcontainer.delete()
 703
 704        tourney_row_height = 200
 705        self._subcontainerheight = (
 706            620 + self._tournament_button_count * tourney_row_height
 707        )
 708
 709        self._subcontainer = bui.containerwidget(
 710            parent=self._scrollwidget,
 711            size=(self._subcontainerwidth, self._subcontainerheight),
 712            background=False,
 713            claims_left_right=True,
 714            claims_tab=True,
 715            selection_loops_to_parent=True,
 716        )
 717
 718        bui.containerwidget(
 719            edit=self._root_widget, selected_child=self._scrollwidget
 720        )
 721        if self._back_button is not None:
 722            bui.containerwidget(
 723                edit=self._root_widget, cancel_button=self._back_button
 724            )
 725
 726        w_parent = self._subcontainer
 727        h_base = 6
 728
 729        v = self._subcontainerheight - 73
 730
 731        self._campaign_percent_text = bui.textwidget(
 732            parent=w_parent,
 733            position=(h_base + 27, v + 30),
 734            size=(0, 0),
 735            text='',
 736            h_align='left',
 737            v_align='center',
 738            color=bui.app.ui_v1.title_color,
 739            scale=1.1,
 740        )
 741
 742        row_v_show_buffer = 100
 743        v -= 198
 744
 745        h_scroll = bui.hscrollwidget(
 746            parent=w_parent,
 747            size=(self._scroll_width - 10, 205),
 748            position=(-5, v),
 749            simple_culling_h=70,
 750            highlight=False,
 751            border_opacity=0.0,
 752            color=(0.45, 0.4, 0.5),
 753            on_select_call=lambda: self._on_row_selected('campaign'),
 754        )
 755        self._campaign_h_scroll = h_scroll
 756        bui.widget(
 757            edit=h_scroll,
 758            show_buffer_top=row_v_show_buffer,
 759            show_buffer_bottom=row_v_show_buffer,
 760            autoselect=True,
 761        )
 762        if self._selected_row == 'campaign':
 763            bui.containerwidget(
 764                edit=w_parent, selected_child=h_scroll, visible_child=h_scroll
 765            )
 766        bui.containerwidget(edit=h_scroll, claims_left_right=True)
 767        self._campaign_sub_container = bui.containerwidget(
 768            parent=h_scroll, size=(180 + 200 * 10, 200), background=False
 769        )
 770
 771        # Tournaments
 772
 773        self._tournament_buttons: list[TournamentButton] = []
 774
 775        v -= 53
 776        # FIXME shouldn't use hard-coded strings here.
 777        txt = bui.Lstr(
 778            resource='tournamentsText', fallback_resource='tournamentText'
 779        ).evaluate()
 780        t_width = bui.get_string_width(txt, suppress_warning=True)
 781        bui.textwidget(
 782            parent=w_parent,
 783            position=(h_base + 27, v + 30),
 784            size=(0, 0),
 785            text=txt,
 786            h_align='left',
 787            v_align='center',
 788            color=bui.app.ui_v1.title_color,
 789            scale=1.1,
 790        )
 791        self._tournament_info_button = bui.buttonwidget(
 792            parent=w_parent,
 793            label='?',
 794            size=(20, 20),
 795            text_scale=0.6,
 796            position=(h_base + 27 + t_width * 1.1 + 15, v + 18),
 797            button_type='square',
 798            color=(0.6, 0.5, 0.65),
 799            textcolor=(0.7, 0.6, 0.75),
 800            autoselect=True,
 801            up_widget=self._campaign_h_scroll,
 802            on_activate_call=self._on_tournament_info_press,
 803        )
 804        bui.widget(
 805            edit=self._tournament_info_button,
 806            left_widget=self._tournament_info_button,
 807            right_widget=self._tournament_info_button,
 808        )
 809
 810        # Say 'unavailable' if there are zero tournaments, and if we're not
 811        # signed in add that as well (that's probably why we see
 812        # no tournaments).
 813        if self._tournament_button_count == 0:
 814            unavailable_text = bui.Lstr(resource='unavailableText')
 815            if plus.get_v1_account_state() != 'signed_in':
 816                unavailable_text = bui.Lstr(
 817                    value='${A} (${B})',
 818                    subs=[
 819                        ('${A}', unavailable_text),
 820                        ('${B}', bui.Lstr(resource='notSignedInText')),
 821                    ],
 822                )
 823            bui.textwidget(
 824                parent=w_parent,
 825                position=(h_base + 47, v),
 826                size=(0, 0),
 827                text=unavailable_text,
 828                h_align='left',
 829                v_align='center',
 830                color=bui.app.ui_v1.title_color,
 831                scale=0.9,
 832            )
 833            v -= 40
 834        v -= 198
 835
 836        tournament_h_scroll = None
 837        if self._tournament_button_count > 0:
 838            for i in range(self._tournament_button_count):
 839                tournament_h_scroll = h_scroll = bui.hscrollwidget(
 840                    parent=w_parent,
 841                    size=(self._scroll_width - 10, 205),
 842                    position=(-5, v),
 843                    highlight=False,
 844                    border_opacity=0.0,
 845                    color=(0.45, 0.4, 0.5),
 846                    on_select_call=bui.Call(
 847                        self._on_row_selected, 'tournament' + str(i + 1)
 848                    ),
 849                )
 850                bui.widget(
 851                    edit=h_scroll,
 852                    show_buffer_top=row_v_show_buffer,
 853                    show_buffer_bottom=row_v_show_buffer,
 854                    autoselect=True,
 855                )
 856                if self._selected_row == 'tournament' + str(i + 1):
 857                    bui.containerwidget(
 858                        edit=w_parent,
 859                        selected_child=h_scroll,
 860                        visible_child=h_scroll,
 861                    )
 862                bui.containerwidget(edit=h_scroll, claims_left_right=True)
 863                sc2 = bui.containerwidget(
 864                    parent=h_scroll,
 865                    size=(self._scroll_width - 24, 200),
 866                    background=False,
 867                )
 868                h = 0
 869                v2 = -2
 870                is_last_sel = True
 871                self._tournament_buttons.append(
 872                    TournamentButton(
 873                        sc2,
 874                        h,
 875                        v2,
 876                        is_last_sel,
 877                        on_pressed=bui.WeakCall(self.run_tournament),
 878                    )
 879                )
 880                v -= 200
 881
 882        # Custom Games. (called 'Practice' in UI these days).
 883        v -= 50
 884        bui.textwidget(
 885            parent=w_parent,
 886            position=(h_base + 27, v + 30 + 198),
 887            size=(0, 0),
 888            text=bui.Lstr(
 889                resource='practiceText',
 890                fallback_resource='coopSelectWindow.customText',
 891            ),
 892            h_align='left',
 893            v_align='center',
 894            color=bui.app.ui_v1.title_color,
 895            scale=1.1,
 896        )
 897
 898        items = [
 899            'Challenges:Infinite Onslaught',
 900            'Challenges:Infinite Runaround',
 901            'Challenges:Ninja Fight',
 902            'Challenges:Pro Ninja Fight',
 903            'Challenges:Meteor Shower',
 904            'Challenges:Target Practice B',
 905            'Challenges:Target Practice',
 906        ]
 907
 908        # Show easter-egg-hunt either if its easter or we own it.
 909        if plus.get_v1_account_misc_read_val(
 910            'easter', False
 911        ) or plus.get_purchased('games.easter_egg_hunt'):
 912            items = [
 913                'Challenges:Easter Egg Hunt',
 914                'Challenges:Pro Easter Egg Hunt',
 915            ] + items
 916
 917        # If we've defined custom games, put them at the beginning.
 918        if bui.app.classic.custom_coop_practice_games:
 919            items = bui.app.classic.custom_coop_practice_games + items
 920
 921        self._custom_h_scroll = custom_h_scroll = h_scroll = bui.hscrollwidget(
 922            parent=w_parent,
 923            size=(self._scroll_width - 10, 205),
 924            position=(-5, v),
 925            highlight=False,
 926            border_opacity=0.0,
 927            color=(0.45, 0.4, 0.5),
 928            on_select_call=bui.Call(self._on_row_selected, 'custom'),
 929        )
 930        bui.widget(
 931            edit=h_scroll,
 932            show_buffer_top=row_v_show_buffer,
 933            show_buffer_bottom=1.5 * row_v_show_buffer,
 934            autoselect=True,
 935        )
 936        if self._selected_row == 'custom':
 937            bui.containerwidget(
 938                edit=w_parent, selected_child=h_scroll, visible_child=h_scroll
 939            )
 940        bui.containerwidget(edit=h_scroll, claims_left_right=True)
 941        sc2 = bui.containerwidget(
 942            parent=h_scroll,
 943            size=(max(self._scroll_width - 24, 30 + 200 * len(items)), 200),
 944            background=False,
 945        )
 946        h_spacing = 200
 947        self._custom_buttons: list[GameButton] = []
 948        h = 0
 949        v2 = -2
 950        for item in items:
 951            is_last_sel = item == self._selected_custom_level
 952            self._custom_buttons.append(
 953                GameButton(self, sc2, item, h, v2, is_last_sel, 'custom')
 954            )
 955            h += h_spacing
 956
 957        # We can't fill in our campaign row until tourney buttons are in place.
 958        # (for wiring up)
 959        self._refresh_campaign_row()
 960
 961        for i, tbutton in enumerate(self._tournament_buttons):
 962            bui.widget(
 963                edit=tbutton.button,
 964                up_widget=self._tournament_info_button
 965                if i == 0
 966                else self._tournament_buttons[i - 1].button,
 967                down_widget=self._tournament_buttons[(i + 1)].button
 968                if i + 1 < len(self._tournament_buttons)
 969                else custom_h_scroll,
 970            )
 971            bui.widget(
 972                edit=tbutton.more_scores_button,
 973                down_widget=self._tournament_buttons[
 974                    (i + 1)
 975                ].current_leader_name_text
 976                if i + 1 < len(self._tournament_buttons)
 977                else custom_h_scroll,
 978            )
 979            bui.widget(
 980                edit=tbutton.current_leader_name_text,
 981                up_widget=self._tournament_info_button
 982                if i == 0
 983                else self._tournament_buttons[i - 1].more_scores_button,
 984            )
 985
 986        for btn in self._custom_buttons:
 987            try:
 988                bui.widget(
 989                    edit=btn.get_button(),
 990                    up_widget=tournament_h_scroll
 991                    if self._tournament_buttons
 992                    else self._tournament_info_button,
 993                )
 994            except Exception:
 995                logging.exception('Error wiring up custom buttons.')
 996
 997        if self._back_button is not None:
 998            bui.buttonwidget(
 999                edit=self._back_button, on_activate_call=self._back
1000            )
1001        else:
1002            bui.containerwidget(
1003                edit=self._root_widget, on_cancel_call=self._back
1004            )
1005
1006        # There's probably several 'onSelected' callbacks pushed onto the
1007        # event queue.. we need to push ours too so we're enabled *after* them.
1008        bui.pushcall(self._enable_selectable_callback)
1009
1010    def _on_row_selected(self, row: str) -> None:
1011        if self._do_selection_callbacks:
1012            if self._selected_row != row:
1013                self._selected_row = row
1014
1015    def _enable_selectable_callback(self) -> None:
1016        self._do_selection_callbacks = True
1017
1018    def _switch_to_league_rankings(self) -> None:
1019        # pylint: disable=cyclic-import
1020        from bauiv1lib.account import show_sign_in_prompt
1021        from bauiv1lib.league.rankwindow import LeagueRankWindow
1022
1023        plus = bui.app.plus
1024        assert plus is not None
1025
1026        if plus.get_v1_account_state() != 'signed_in':
1027            show_sign_in_prompt()
1028            return
1029        self._save_state()
1030        bui.containerwidget(edit=self._root_widget, transition='out_left')
1031        assert self._league_rank_button is not None
1032        assert bui.app.classic is not None
1033        bui.app.ui_v1.set_main_menu_window(
1034            LeagueRankWindow(
1035                origin_widget=self._league_rank_button.get_button()
1036            ).get_root_widget()
1037        )
1038
1039    def _switch_to_score(
1040        self,
1041        show_tab: StoreBrowserWindow.TabID
1042        | None = StoreBrowserWindow.TabID.EXTRAS,
1043    ) -> None:
1044        # pylint: disable=cyclic-import
1045        from bauiv1lib.account import show_sign_in_prompt
1046
1047        plus = bui.app.plus
1048        assert plus is not None
1049
1050        if plus.get_v1_account_state() != 'signed_in':
1051            show_sign_in_prompt()
1052            return
1053        self._save_state()
1054        bui.containerwidget(edit=self._root_widget, transition='out_left')
1055        assert self._store_button is not None
1056        assert bui.app.classic is not None
1057        bui.app.ui_v1.set_main_menu_window(
1058            StoreBrowserWindow(
1059                origin_widget=self._store_button.get_button(),
1060                show_tab=show_tab,
1061                back_location='CoopBrowserWindow',
1062            ).get_root_widget()
1063        )
1064
1065    def is_tourney_data_up_to_date(self) -> bool:
1066        """Return whether our tourney data is up to date."""
1067        return self._tourney_data_up_to_date
1068
1069    def run_game(self, game: str) -> None:
1070        """Run the provided game."""
1071        # pylint: disable=too-many-branches
1072        # pylint: disable=cyclic-import
1073        from bauiv1lib.confirm import ConfirmWindow
1074        from bauiv1lib.purchase import PurchaseWindow
1075        from bauiv1lib.account import show_sign_in_prompt
1076
1077        plus = bui.app.plus
1078        assert plus is not None
1079
1080        assert bui.app.classic is not None
1081
1082        args: dict[str, Any] = {}
1083
1084        if game == 'Easy:The Last Stand':
1085            ConfirmWindow(
1086                bui.Lstr(
1087                    resource='difficultyHardUnlockOnlyText',
1088                    fallback_resource='difficultyHardOnlyText',
1089                ),
1090                cancel_button=False,
1091                width=460,
1092                height=130,
1093            )
1094            return
1095
1096        # Infinite onslaught/runaround require pro; bring up a store link
1097        # if need be.
1098        if (
1099            game
1100            in (
1101                'Challenges:Infinite Runaround',
1102                'Challenges:Infinite Onslaught',
1103            )
1104            and not bui.app.classic.accounts.have_pro()
1105        ):
1106            if plus.get_v1_account_state() != 'signed_in':
1107                show_sign_in_prompt()
1108            else:
1109                PurchaseWindow(items=['pro'])
1110            return
1111
1112        required_purchase: str | None
1113        if game in ['Challenges:Meteor Shower']:
1114            required_purchase = 'games.meteor_shower'
1115        elif game in [
1116            'Challenges:Target Practice',
1117            'Challenges:Target Practice B',
1118        ]:
1119            required_purchase = 'games.target_practice'
1120        elif game in ['Challenges:Ninja Fight']:
1121            required_purchase = 'games.ninja_fight'
1122        elif game in ['Challenges:Pro Ninja Fight']:
1123            required_purchase = 'games.ninja_fight'
1124        elif game in [
1125            'Challenges:Easter Egg Hunt',
1126            'Challenges:Pro Easter Egg Hunt',
1127        ]:
1128            required_purchase = 'games.easter_egg_hunt'
1129        else:
1130            required_purchase = None
1131
1132        if required_purchase is not None and not plus.get_purchased(
1133            required_purchase
1134        ):
1135            if plus.get_v1_account_state() != 'signed_in':
1136                show_sign_in_prompt()
1137            else:
1138                PurchaseWindow(items=[required_purchase])
1139            return
1140
1141        self._save_state()
1142
1143        if bui.app.classic.launch_coop_game(game, args=args):
1144            bui.containerwidget(edit=self._root_widget, transition='out_left')
1145
1146    def run_tournament(self, tournament_button: TournamentButton) -> None:
1147        """Run the provided tournament game."""
1148        from bauiv1lib.account import show_sign_in_prompt
1149        from bauiv1lib.tournamententry import TournamentEntryWindow
1150
1151        plus = bui.app.plus
1152        assert plus is not None
1153
1154        if plus.get_v1_account_state() != 'signed_in':
1155            show_sign_in_prompt()
1156            return
1157
1158        if bui.workspaces_in_use():
1159            bui.screenmessage(
1160                bui.Lstr(resource='tournamentsDisabledWorkspaceText'),
1161                color=(1, 0, 0),
1162            )
1163            bui.getsound('error').play()
1164            return
1165
1166        if not self._tourney_data_up_to_date:
1167            bui.screenmessage(
1168                bui.Lstr(resource='tournamentCheckingStateText'),
1169                color=(1, 1, 0),
1170            )
1171            bui.getsound('error').play()
1172            return
1173
1174        if tournament_button.tournament_id is None:
1175            bui.screenmessage(
1176                bui.Lstr(resource='internal.unavailableNoConnectionText'),
1177                color=(1, 0, 0),
1178            )
1179            bui.getsound('error').play()
1180            return
1181
1182        if tournament_button.required_league is not None:
1183            bui.screenmessage(
1184                bui.Lstr(
1185                    resource='league.tournamentLeagueText',
1186                    subs=[
1187                        (
1188                            '${NAME}',
1189                            bui.Lstr(
1190                                translate=(
1191                                    'leagueNames',
1192                                    tournament_button.required_league,
1193                                )
1194                            ),
1195                        )
1196                    ],
1197                ),
1198                color=(1, 0, 0),
1199            )
1200            bui.getsound('error').play()
1201            return
1202
1203        if tournament_button.time_remaining <= 0:
1204            bui.screenmessage(
1205                bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0)
1206            )
1207            bui.getsound('error').play()
1208            return
1209
1210        self._save_state()
1211
1212        assert tournament_button.tournament_id is not None
1213        TournamentEntryWindow(
1214            tournament_id=tournament_button.tournament_id,
1215            position=tournament_button.button.get_screen_space_center(),
1216        )
1217
1218    def _back(self) -> None:
1219        # pylint: disable=cyclic-import
1220        from bauiv1lib.play import PlayWindow
1221
1222        # If something is selected, store it.
1223        self._save_state()
1224        bui.containerwidget(
1225            edit=self._root_widget, transition=self._transition_out
1226        )
1227        assert bui.app.classic is not None
1228        bui.app.ui_v1.set_main_menu_window(
1229            PlayWindow(transition='in_left').get_root_widget()
1230        )
1231
1232    def _save_state(self) -> None:
1233        cfg = bui.app.config
1234        try:
1235            sel = self._root_widget.get_selected_child()
1236            if sel == self._back_button:
1237                sel_name = 'Back'
1238            elif sel == self._store_button_widget:
1239                sel_name = 'Store'
1240            elif sel == self._league_rank_button_widget:
1241                sel_name = 'PowerRanking'
1242            elif sel == self._scrollwidget:
1243                sel_name = 'Scroll'
1244            else:
1245                raise ValueError('unrecognized selection')
1246            assert bui.app.classic is not None
1247            bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name}
1248        except Exception:
1249            logging.exception('Error saving state for %s.', self)
1250
1251        cfg['Selected Coop Row'] = self._selected_row
1252        cfg['Selected Coop Custom Level'] = self._selected_custom_level
1253        cfg['Selected Coop Campaign Level'] = self._selected_campaign_level
1254        cfg.commit()
1255
1256    def _restore_state(self) -> None:
1257        try:
1258            assert bui.app.classic is not None
1259            sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get(
1260                'sel_name'
1261            )
1262            if sel_name == 'Back':
1263                sel = self._back_button
1264            elif sel_name == 'Scroll':
1265                sel = self._scrollwidget
1266            elif sel_name == 'PowerRanking':
1267                sel = self._league_rank_button_widget
1268            elif sel_name == 'Store':
1269                sel = self._store_button_widget
1270            else:
1271                sel = self._scrollwidget
1272            bui.containerwidget(edit=self._root_widget, selected_child=sel)
1273        except Exception:
1274            logging.exception('Error restoring state for %s.', self)
1275
1276    def sel_change(self, row: str, game: str) -> None:
1277        """(internal)"""
1278        if self._do_selection_callbacks:
1279            if row == 'custom':
1280                self._selected_custom_level = game
1281            elif row == 'campaign':
1282                self._selected_campaign_level = game

Window for browsing co-op levels/games/etc.

CoopBrowserWindow( transition: str | None = 'in_right', origin_widget: _bauiv1.Widget | None = None)
 28    def __init__(
 29        self,
 30        transition: str | None = 'in_right',
 31        origin_widget: bui.Widget | None = None,
 32    ):
 33        # pylint: disable=too-many-statements
 34        # pylint: disable=cyclic-import
 35
 36        plus = bui.app.plus
 37        assert plus is not None
 38
 39        # Preload some modules we use in a background thread so we won't
 40        # have a visual hitch when the user taps them.
 41        Thread(target=self._preload_modules).start()
 42
 43        bui.set_analytics_screen('Coop Window')
 44
 45        app = bui.app
 46        assert app.classic is not None
 47        cfg = app.config
 48
 49        # Quick note to players that tourneys won't work in ballistica
 50        # core builds. (need to split the word so it won't get subbed out)
 51        if 'ballistica' + 'kit' == bui.appname():
 52            bui.apptimer(
 53                1.0,
 54                lambda: bui.screenmessage(
 55                    bui.Lstr(resource='noTournamentsInTestBuildText'),
 56                    color=(1, 1, 0),
 57                ),
 58            )
 59
 60        # If they provided an origin-widget, scale up from that.
 61        scale_origin: tuple[float, float] | None
 62        if origin_widget is not None:
 63            self._transition_out = 'out_scale'
 64            scale_origin = origin_widget.get_screen_space_center()
 65            transition = 'in_scale'
 66        else:
 67            self._transition_out = 'out_right'
 68            scale_origin = None
 69
 70        # Try to recreate the same number of buttons we had last time so our
 71        # re-selection code works.
 72        self._tournament_button_count = app.config.get('Tournament Rows', 0)
 73        assert isinstance(self._tournament_button_count, int)
 74
 75        self.star_tex = bui.gettexture('star')
 76        self.lsbt = bui.getmesh('level_select_button_transparent')
 77        self.lsbo = bui.getmesh('level_select_button_opaque')
 78        self.a_outline_tex = bui.gettexture('achievementOutline')
 79        self.a_outline_mesh = bui.getmesh('achievementOutline')
 80        self._campaign_sub_container: bui.Widget | None = None
 81        self._tournament_info_button: bui.Widget | None = None
 82        self._easy_button: bui.Widget | None = None
 83        self._hard_button: bui.Widget | None = None
 84        self._hard_button_lock_image: bui.Widget | None = None
 85        self._campaign_percent_text: bui.Widget | None = None
 86
 87        assert bui.app.classic is not None
 88        uiscale = bui.app.ui_v1.uiscale
 89        self._width = 1320 if uiscale is bui.UIScale.SMALL else 1120
 90        self._x_inset = x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
 91        self._height = (
 92            657
 93            if uiscale is bui.UIScale.SMALL
 94            else 730
 95            if uiscale is bui.UIScale.MEDIUM
 96            else 800
 97        )
 98        app.ui_v1.set_main_menu_location('Coop Select')
 99        self._r = 'coopSelectWindow'
100        top_extra = 20 if uiscale is bui.UIScale.SMALL else 0
101
102        self._tourney_data_up_to_date = False
103
104        self._campaign_difficulty = plus.get_v1_account_misc_val(
105            'campaignDifficulty', 'easy'
106        )
107
108        super().__init__(
109            root_widget=bui.containerwidget(
110                size=(self._width, self._height + top_extra),
111                toolbar_visibility='menu_full',
112                scale_origin_stack_offset=scale_origin,
113                stack_offset=(
114                    (0, -15)
115                    if uiscale is bui.UIScale.SMALL
116                    else (0, 0)
117                    if uiscale is bui.UIScale.MEDIUM
118                    else (0, 0)
119                ),
120                transition=transition,
121                scale=(
122                    1.2
123                    if uiscale is bui.UIScale.SMALL
124                    else 0.8
125                    if uiscale is bui.UIScale.MEDIUM
126                    else 0.75
127                ),
128            )
129        )
130
131        if app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL:
132            self._back_button = None
133        else:
134            self._back_button = bui.buttonwidget(
135                parent=self._root_widget,
136                position=(
137                    75 + x_inset,
138                    self._height
139                    - 87
140                    - (4 if uiscale is bui.UIScale.SMALL else 0),
141                ),
142                size=(120, 60),
143                scale=1.2,
144                autoselect=True,
145                label=bui.Lstr(resource='backText'),
146                button_type='back',
147            )
148
149        self._league_rank_button: LeagueRankButton | None
150        self._store_button: StoreButton | None
151        self._store_button_widget: bui.Widget | None
152        self._league_rank_button_widget: bui.Widget | None
153
154        if not app.ui_v1.use_toolbars:
155            prb = self._league_rank_button = LeagueRankButton(
156                parent=self._root_widget,
157                position=(
158                    self._width - (282 + x_inset),
159                    self._height
160                    - 85
161                    - (4 if uiscale is bui.UIScale.SMALL else 0),
162                ),
163                size=(100, 60),
164                color=(0.4, 0.4, 0.9),
165                textcolor=(0.9, 0.9, 2.0),
166                scale=1.05,
167                on_activate_call=bui.WeakCall(self._switch_to_league_rankings),
168            )
169            self._league_rank_button_widget = prb.get_button()
170
171            sbtn = self._store_button = StoreButton(
172                parent=self._root_widget,
173                position=(
174                    self._width - (170 + x_inset),
175                    self._height
176                    - 85
177                    - (4 if uiscale is bui.UIScale.SMALL else 0),
178                ),
179                size=(100, 60),
180                color=(0.6, 0.4, 0.7),
181                show_tickets=True,
182                button_type='square',
183                sale_scale=0.85,
184                textcolor=(0.9, 0.7, 1.0),
185                scale=1.05,
186                on_activate_call=bui.WeakCall(self._switch_to_score, None),
187            )
188            self._store_button_widget = sbtn.get_button()
189            bui.widget(
190                edit=self._back_button,
191                right_widget=self._league_rank_button_widget,
192            )
193            bui.widget(
194                edit=self._league_rank_button_widget,
195                left_widget=self._back_button,
196            )
197        else:
198            self._league_rank_button = None
199            self._store_button = None
200            self._store_button_widget = None
201            self._league_rank_button_widget = None
202
203        # Move our corner buttons dynamically to keep them out of the way of
204        # the party icon :-(
205        self._update_corner_button_positions()
206        self._update_corner_button_positions_timer = bui.AppTimer(
207            1.0, bui.WeakCall(self._update_corner_button_positions), repeat=True
208        )
209
210        self._last_tournament_query_time: float | None = None
211        self._last_tournament_query_response_time: float | None = None
212        self._doing_tournament_query = False
213
214        self._selected_campaign_level = cfg.get(
215            'Selected Coop Campaign Level', None
216        )
217        self._selected_custom_level = cfg.get(
218            'Selected Coop Custom Level', None
219        )
220
221        # Don't want initial construction affecting our last-selected.
222        self._do_selection_callbacks = False
223        v = self._height - 95
224        txt = bui.textwidget(
225            parent=self._root_widget,
226            position=(
227                self._width * 0.5,
228                v + 40 - (0 if uiscale is bui.UIScale.SMALL else 0),
229            ),
230            size=(0, 0),
231            text=bui.Lstr(
232                resource='playModes.singlePlayerCoopText',
233                fallback_resource='playModes.coopText',
234            ),
235            h_align='center',
236            color=app.ui_v1.title_color,
237            scale=1.5,
238            maxwidth=500,
239            v_align='center',
240        )
241
242        if app.ui_v1.use_toolbars and uiscale is bui.UIScale.SMALL:
243            bui.textwidget(edit=txt, text='')
244
245        if self._back_button is not None:
246            bui.buttonwidget(
247                edit=self._back_button,
248                button_type='backSmall',
249                size=(60, 50),
250                position=(
251                    75 + x_inset,
252                    self._height
253                    - 87
254                    - (4 if uiscale is bui.UIScale.SMALL else 0)
255                    + 6,
256                ),
257                label=bui.charstr(bui.SpecialChar.BACK),
258            )
259
260        self._selected_row = cfg.get('Selected Coop Row', None)
261
262        self._scroll_width = self._width - (130 + 2 * x_inset)
263        self._scroll_height = self._height - (
264            190
265            if uiscale is bui.UIScale.SMALL and app.ui_v1.use_toolbars
266            else 160
267        )
268
269        self._subcontainerwidth = 800.0
270        self._subcontainerheight = 1400.0
271
272        self._scrollwidget = bui.scrollwidget(
273            parent=self._root_widget,
274            highlight=False,
275            position=(65 + x_inset, 120)
276            if uiscale is bui.UIScale.SMALL and app.ui_v1.use_toolbars
277            else (65 + x_inset, 70),
278            size=(self._scroll_width, self._scroll_height),
279            simple_culling_v=10.0,
280            claims_left_right=True,
281            claims_tab=True,
282            selection_loops_to_parent=True,
283        )
284        self._subcontainer: bui.Widget | None = None
285
286        # Take note of our account state; we'll refresh later if this changes.
287        self._account_state_num = plus.get_v1_account_state_num()
288
289        # Same for fg/bg state.
290        self._fg_state = app.fg_state
291
292        self._refresh()
293        self._restore_state()
294
295        # Even though we might display cached tournament data immediately, we
296        # don't consider it valid until we've pinged.
297        # the server for an update
298        self._tourney_data_up_to_date = False
299
300        # If we've got a cached tournament list for our account and info for
301        # each one of those tournaments, go ahead and display it as a
302        # starting point.
303        if (
304            app.classic.accounts.account_tournament_list is not None
305            and app.classic.accounts.account_tournament_list[0]
306            == plus.get_v1_account_state_num()
307            and all(
308                t_id in app.classic.accounts.tournament_info
309                for t_id in app.classic.accounts.account_tournament_list[1]
310            )
311        ):
312            tourney_data = [
313                app.classic.accounts.tournament_info[t_id]
314                for t_id in app.classic.accounts.account_tournament_list[1]
315            ]
316            self._update_for_data(tourney_data)
317
318        # This will pull new data periodically, update timers, etc.
319        self._update_timer = bui.AppTimer(
320            1.0, bui.WeakCall(self._update), repeat=True
321        )
322        self._update()
star_tex
lsbt
lsbo
a_outline_tex
a_outline_mesh
def is_tourney_data_up_to_date(self) -> bool:
1065    def is_tourney_data_up_to_date(self) -> bool:
1066        """Return whether our tourney data is up to date."""
1067        return self._tourney_data_up_to_date

Return whether our tourney data is up to date.

def run_game(self, game: str) -> None:
1069    def run_game(self, game: str) -> None:
1070        """Run the provided game."""
1071        # pylint: disable=too-many-branches
1072        # pylint: disable=cyclic-import
1073        from bauiv1lib.confirm import ConfirmWindow
1074        from bauiv1lib.purchase import PurchaseWindow
1075        from bauiv1lib.account import show_sign_in_prompt
1076
1077        plus = bui.app.plus
1078        assert plus is not None
1079
1080        assert bui.app.classic is not None
1081
1082        args: dict[str, Any] = {}
1083
1084        if game == 'Easy:The Last Stand':
1085            ConfirmWindow(
1086                bui.Lstr(
1087                    resource='difficultyHardUnlockOnlyText',
1088                    fallback_resource='difficultyHardOnlyText',
1089                ),
1090                cancel_button=False,
1091                width=460,
1092                height=130,
1093            )
1094            return
1095
1096        # Infinite onslaught/runaround require pro; bring up a store link
1097        # if need be.
1098        if (
1099            game
1100            in (
1101                'Challenges:Infinite Runaround',
1102                'Challenges:Infinite Onslaught',
1103            )
1104            and not bui.app.classic.accounts.have_pro()
1105        ):
1106            if plus.get_v1_account_state() != 'signed_in':
1107                show_sign_in_prompt()
1108            else:
1109                PurchaseWindow(items=['pro'])
1110            return
1111
1112        required_purchase: str | None
1113        if game in ['Challenges:Meteor Shower']:
1114            required_purchase = 'games.meteor_shower'
1115        elif game in [
1116            'Challenges:Target Practice',
1117            'Challenges:Target Practice B',
1118        ]:
1119            required_purchase = 'games.target_practice'
1120        elif game in ['Challenges:Ninja Fight']:
1121            required_purchase = 'games.ninja_fight'
1122        elif game in ['Challenges:Pro Ninja Fight']:
1123            required_purchase = 'games.ninja_fight'
1124        elif game in [
1125            'Challenges:Easter Egg Hunt',
1126            'Challenges:Pro Easter Egg Hunt',
1127        ]:
1128            required_purchase = 'games.easter_egg_hunt'
1129        else:
1130            required_purchase = None
1131
1132        if required_purchase is not None and not plus.get_purchased(
1133            required_purchase
1134        ):
1135            if plus.get_v1_account_state() != 'signed_in':
1136                show_sign_in_prompt()
1137            else:
1138                PurchaseWindow(items=[required_purchase])
1139            return
1140
1141        self._save_state()
1142
1143        if bui.app.classic.launch_coop_game(game, args=args):
1144            bui.containerwidget(edit=self._root_widget, transition='out_left')

Run the provided game.

def run_tournament( self, tournament_button: bauiv1lib.coop.tournamentbutton.TournamentButton) -> None:
1146    def run_tournament(self, tournament_button: TournamentButton) -> None:
1147        """Run the provided tournament game."""
1148        from bauiv1lib.account import show_sign_in_prompt
1149        from bauiv1lib.tournamententry import TournamentEntryWindow
1150
1151        plus = bui.app.plus
1152        assert plus is not None
1153
1154        if plus.get_v1_account_state() != 'signed_in':
1155            show_sign_in_prompt()
1156            return
1157
1158        if bui.workspaces_in_use():
1159            bui.screenmessage(
1160                bui.Lstr(resource='tournamentsDisabledWorkspaceText'),
1161                color=(1, 0, 0),
1162            )
1163            bui.getsound('error').play()
1164            return
1165
1166        if not self._tourney_data_up_to_date:
1167            bui.screenmessage(
1168                bui.Lstr(resource='tournamentCheckingStateText'),
1169                color=(1, 1, 0),
1170            )
1171            bui.getsound('error').play()
1172            return
1173
1174        if tournament_button.tournament_id is None:
1175            bui.screenmessage(
1176                bui.Lstr(resource='internal.unavailableNoConnectionText'),
1177                color=(1, 0, 0),
1178            )
1179            bui.getsound('error').play()
1180            return
1181
1182        if tournament_button.required_league is not None:
1183            bui.screenmessage(
1184                bui.Lstr(
1185                    resource='league.tournamentLeagueText',
1186                    subs=[
1187                        (
1188                            '${NAME}',
1189                            bui.Lstr(
1190                                translate=(
1191                                    'leagueNames',
1192                                    tournament_button.required_league,
1193                                )
1194                            ),
1195                        )
1196                    ],
1197                ),
1198                color=(1, 0, 0),
1199            )
1200            bui.getsound('error').play()
1201            return
1202
1203        if tournament_button.time_remaining <= 0:
1204            bui.screenmessage(
1205                bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0)
1206            )
1207            bui.getsound('error').play()
1208            return
1209
1210        self._save_state()
1211
1212        assert tournament_button.tournament_id is not None
1213        TournamentEntryWindow(
1214            tournament_id=tournament_button.tournament_id,
1215            position=tournament_button.button.get_screen_space_center(),
1216        )

Run the provided tournament game.

Inherited Members
bauiv1._uitypes.Window
get_root_widget