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