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