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