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