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