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

Create a MainWindow given a root widget and transition info.

Automatically handles in and out transitions on the provided widget, so there is no need to set transitions when creating it.

star_tex
lsbt
lsbo
a_outline_tex
a_outline_mesh
@override
def get_main_window_state(self) -> bauiv1.MainWindowState:
317    @override
318    def get_main_window_state(self) -> bui.MainWindowState:
319        # Support recreating our window for back/refresh purposes.
320        cls = type(self)
321        return bui.BasicMainWindowState(
322            create_call=lambda transition, origin_widget: cls(
323                transition=transition, origin_widget=origin_widget
324            )
325        )

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
327    @override
328    def on_main_window_close(self) -> None:
329        self._save_state()

Called before transitioning out a main window.

A good opportunity to save window state/etc.

def is_tourney_data_up_to_date(self) -> bool:
1075    def is_tourney_data_up_to_date(self) -> bool:
1076        """Return whether our tourney data is up to date."""
1077        return self._tourney_data_up_to_date

Return whether our tourney data is up to date.

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

Run the provided game.

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

Run the provided tournament game.

Inherited Members
bauiv1._uitypes.MainWindow
main_window_back_state
main_window_close
can_change_main_window
main_window_back
main_window_replace
bauiv1._uitypes.Window
get_root_widget