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.
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:
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