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