bastd.ui.mainmenu
Implements the main menu window.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Implements the main menu window.""" 4# pylint: disable=too-many-lines 5 6from __future__ import annotations 7 8from typing import TYPE_CHECKING 9 10import ba 11import ba.internal 12 13if TYPE_CHECKING: 14 from typing import Any, Callable 15 16 17class MainMenuWindow(ba.Window): 18 """The main menu window, both in-game and in the main menu session.""" 19 20 def __init__(self, transition: str | None = 'in_right'): 21 # pylint: disable=cyclic-import 22 import threading 23 from bastd.mainmenu import MainMenuSession 24 25 self._in_game = not isinstance( 26 ba.internal.get_foreground_host_session(), MainMenuSession 27 ) 28 29 # Preload some modules we use in a background thread so we won't 30 # have a visual hitch when the user taps them. 31 threading.Thread(target=self._preload_modules).start() 32 33 if not self._in_game: 34 ba.set_analytics_screen('Main Menu') 35 self._show_remote_app_info_on_first_launch() 36 37 # Make a vanilla container; we'll modify it to our needs in refresh. 38 super().__init__( 39 root_widget=ba.containerwidget( 40 transition=transition, 41 toolbar_visibility='menu_minimal_no_back' 42 if self._in_game 43 else 'menu_minimal_no_back', 44 ) 45 ) 46 47 # Grab this stuff in case it changes. 48 self._is_demo = ba.app.demo_mode 49 self._is_arcade = ba.app.arcade_mode 50 self._is_iircade = ba.app.iircade_mode 51 52 self._tdelay = 0.0 53 self._t_delay_inc = 0.02 54 self._t_delay_play = 1.7 55 self._p_index = 0 56 self._use_autoselect = True 57 self._button_width = 200.0 58 self._button_height = 45.0 59 self._width = 100.0 60 self._height = 100.0 61 self._demo_menu_button: ba.Widget | None = None 62 self._gather_button: ba.Widget | None = None 63 self._start_button: ba.Widget | None = None 64 self._watch_button: ba.Widget | None = None 65 self._account_button: ba.Widget | None = None 66 self._how_to_play_button: ba.Widget | None = None 67 self._credits_button: ba.Widget | None = None 68 self._settings_button: ba.Widget | None = None 69 self._next_refresh_allow_time = 0.0 70 71 self._store_char_tex = self._get_store_char_tex() 72 73 self._refresh() 74 self._restore_state() 75 76 # Keep an eye on a few things and refresh if they change. 77 self._account_state = ba.internal.get_v1_account_state() 78 self._account_state_num = ba.internal.get_v1_account_state_num() 79 self._account_type = ( 80 ba.internal.get_v1_account_type() 81 if self._account_state == 'signed_in' 82 else None 83 ) 84 self._refresh_timer = ba.Timer( 85 0.27, 86 ba.WeakCall(self._check_refresh), 87 repeat=True, 88 timetype=ba.TimeType.REAL, 89 ) 90 91 # noinspection PyUnresolvedReferences 92 @staticmethod 93 def _preload_modules() -> None: 94 """Preload modules we use (called in bg thread).""" 95 import bastd.ui.getremote as _unused 96 import bastd.ui.confirm as _unused2 97 import bastd.ui.store.button as _unused3 98 import bastd.ui.kiosk as _unused4 99 import bastd.ui.account.settings as _unused5 100 import bastd.ui.store.browser as _unused6 101 import bastd.ui.creditslist as _unused7 102 import bastd.ui.helpui as _unused8 103 import bastd.ui.settings.allsettings as _unused9 104 import bastd.ui.gather as _unused10 105 import bastd.ui.watch as _unused11 106 import bastd.ui.play as _unused12 107 108 def _show_remote_app_info_on_first_launch(self) -> None: 109 # The first time the non-in-game menu pops up, we might wanna show 110 # a 'get-remote-app' dialog in front of it. 111 if ba.app.first_main_menu: 112 ba.app.first_main_menu = False 113 try: 114 app = ba.app 115 force_test = False 116 ba.internal.get_local_active_input_devices_count() 117 if ( 118 (app.on_tv or app.platform == 'mac') 119 and ba.app.config.get('launchCount', 0) <= 1 120 ) or force_test: 121 122 def _check_show_bs_remote_window() -> None: 123 try: 124 from bastd.ui.getremote import GetBSRemoteWindow 125 126 ba.playsound(ba.getsound('swish')) 127 GetBSRemoteWindow() 128 except Exception: 129 ba.print_exception( 130 'Error showing get-remote window.' 131 ) 132 133 ba.timer( 134 2.5, 135 _check_show_bs_remote_window, 136 timetype=ba.TimeType.REAL, 137 ) 138 except Exception: 139 ba.print_exception('Error showing get-remote-app info') 140 141 def _get_store_char_tex(self) -> str: 142 return ( 143 'storeCharacterXmas' 144 if ba.internal.get_v1_account_misc_read_val('xmas', False) 145 else 'storeCharacterEaster' 146 if ba.internal.get_v1_account_misc_read_val('easter', False) 147 else 'storeCharacter' 148 ) 149 150 def _check_refresh(self) -> None: 151 if not self._root_widget: 152 return 153 154 now = ba.time(ba.TimeType.REAL) 155 if now < self._next_refresh_allow_time: 156 return 157 158 # Don't refresh for the first few seconds the game is up so we don't 159 # interrupt the transition in. 160 # ba.app.main_menu_window_refresh_check_count += 1 161 # if ba.app.main_menu_window_refresh_check_count < 4: 162 # return 163 164 store_char_tex = self._get_store_char_tex() 165 account_state_num = ba.internal.get_v1_account_state_num() 166 if ( 167 account_state_num != self._account_state_num 168 or store_char_tex != self._store_char_tex 169 ): 170 self._store_char_tex = store_char_tex 171 self._account_state_num = account_state_num 172 account_state = ( 173 self._account_state 174 ) = ba.internal.get_v1_account_state() 175 self._account_type = ( 176 ba.internal.get_v1_account_type() 177 if account_state == 'signed_in' 178 else None 179 ) 180 self._save_state() 181 self._refresh() 182 self._restore_state() 183 184 def get_play_button(self) -> ba.Widget | None: 185 """Return the play button.""" 186 return self._start_button 187 188 def _refresh(self) -> None: 189 # pylint: disable=too-many-branches 190 # pylint: disable=too-many-locals 191 # pylint: disable=too-many-statements 192 from bastd.ui.confirm import QuitWindow 193 from bastd.ui.store.button import StoreButton 194 195 # Clear everything that was there. 196 children = self._root_widget.get_children() 197 for child in children: 198 child.delete() 199 200 self._tdelay = 0.0 201 self._t_delay_inc = 0.0 202 self._t_delay_play = 0.0 203 self._button_width = 200.0 204 self._button_height = 45.0 205 206 self._r = 'mainMenu' 207 208 app = ba.app 209 self._have_quit_button = app.ui.uiscale is ba.UIScale.LARGE or ( 210 app.platform == 'windows' and app.subplatform == 'oculus' 211 ) 212 213 self._have_store_button = not self._in_game 214 215 self._have_settings_button = ( 216 not self._in_game or not app.toolbar_test 217 ) and not (self._is_demo or self._is_arcade or self._is_iircade) 218 219 self._input_device = input_device = ba.internal.get_ui_input_device() 220 self._input_player = input_device.player if input_device else None 221 self._connected_to_remote_player = ( 222 input_device.is_connected_to_remote_player() 223 if input_device 224 else False 225 ) 226 227 positions: list[tuple[float, float, float]] = [] 228 self._p_index = 0 229 230 if self._in_game: 231 h, v, scale = self._refresh_in_game(positions) 232 else: 233 h, v, scale = self._refresh_not_in_game(positions) 234 235 if self._have_settings_button: 236 h, v, scale = positions[self._p_index] 237 self._p_index += 1 238 self._settings_button = ba.buttonwidget( 239 parent=self._root_widget, 240 position=(h - self._button_width * 0.5 * scale, v), 241 size=(self._button_width, self._button_height), 242 scale=scale, 243 autoselect=self._use_autoselect, 244 label=ba.Lstr(resource=self._r + '.settingsText'), 245 transition_delay=self._tdelay, 246 on_activate_call=self._settings, 247 ) 248 249 # Scattered eggs on easter. 250 if ( 251 ba.internal.get_v1_account_misc_read_val('easter', False) 252 and not self._in_game 253 ): 254 icon_size = 34 255 ba.imagewidget( 256 parent=self._root_widget, 257 position=( 258 h - icon_size * 0.5 - 15, 259 v + self._button_height * scale - icon_size * 0.24 + 1.5, 260 ), 261 transition_delay=self._tdelay, 262 size=(icon_size, icon_size), 263 texture=ba.gettexture('egg3'), 264 tilt_scale=0.0, 265 ) 266 267 self._tdelay += self._t_delay_inc 268 269 if self._in_game: 270 h, v, scale = positions[self._p_index] 271 self._p_index += 1 272 273 # If we're in a replay, we have a 'Leave Replay' button. 274 if ba.internal.is_in_replay(): 275 ba.buttonwidget( 276 parent=self._root_widget, 277 position=(h - self._button_width * 0.5 * scale, v), 278 scale=scale, 279 size=(self._button_width, self._button_height), 280 autoselect=self._use_autoselect, 281 label=ba.Lstr(resource='replayEndText'), 282 on_activate_call=self._confirm_end_replay, 283 ) 284 elif ba.internal.get_foreground_host_session() is not None: 285 ba.buttonwidget( 286 parent=self._root_widget, 287 position=(h - self._button_width * 0.5 * scale, v), 288 scale=scale, 289 size=(self._button_width, self._button_height), 290 autoselect=self._use_autoselect, 291 label=ba.Lstr( 292 resource=self._r 293 + ( 294 '.endTestText' 295 if self._is_benchmark() 296 else '.endGameText' 297 ) 298 ), 299 on_activate_call=( 300 self._confirm_end_test 301 if self._is_benchmark() 302 else self._confirm_end_game 303 ), 304 ) 305 # Assume we're in a client-session. 306 else: 307 ba.buttonwidget( 308 parent=self._root_widget, 309 position=(h - self._button_width * 0.5 * scale, v), 310 scale=scale, 311 size=(self._button_width, self._button_height), 312 autoselect=self._use_autoselect, 313 label=ba.Lstr(resource=self._r + '.leavePartyText'), 314 on_activate_call=self._confirm_leave_party, 315 ) 316 317 self._store_button: ba.Widget | None 318 if self._have_store_button: 319 this_b_width = self._button_width 320 h, v, scale = positions[self._p_index] 321 self._p_index += 1 322 323 sbtn = self._store_button_instance = StoreButton( 324 parent=self._root_widget, 325 position=(h - this_b_width * 0.5 * scale, v), 326 size=(this_b_width, self._button_height), 327 scale=scale, 328 on_activate_call=ba.WeakCall(self._on_store_pressed), 329 sale_scale=1.3, 330 transition_delay=self._tdelay, 331 ) 332 self._store_button = store_button = sbtn.get_button() 333 uiscale = ba.app.ui.uiscale 334 icon_size = ( 335 55 336 if uiscale is ba.UIScale.SMALL 337 else 55 338 if uiscale is ba.UIScale.MEDIUM 339 else 70 340 ) 341 ba.imagewidget( 342 parent=self._root_widget, 343 position=( 344 h - icon_size * 0.5, 345 v + self._button_height * scale - icon_size * 0.23, 346 ), 347 transition_delay=self._tdelay, 348 size=(icon_size, icon_size), 349 texture=ba.gettexture(self._store_char_tex), 350 tilt_scale=0.0, 351 draw_controller=store_button, 352 ) 353 354 self._tdelay += self._t_delay_inc 355 else: 356 self._store_button = None 357 358 self._quit_button: ba.Widget | None 359 if not self._in_game and self._have_quit_button: 360 h, v, scale = positions[self._p_index] 361 self._p_index += 1 362 self._quit_button = quit_button = ba.buttonwidget( 363 parent=self._root_widget, 364 autoselect=self._use_autoselect, 365 position=(h - self._button_width * 0.5 * scale, v), 366 size=(self._button_width, self._button_height), 367 scale=scale, 368 label=ba.Lstr( 369 resource=self._r 370 + ( 371 '.quitText' 372 if 'Mac' in ba.app.user_agent_string 373 else '.exitGameText' 374 ) 375 ), 376 on_activate_call=self._quit, 377 transition_delay=self._tdelay, 378 ) 379 380 # Scattered eggs on easter. 381 if ba.internal.get_v1_account_misc_read_val('easter', False): 382 icon_size = 30 383 ba.imagewidget( 384 parent=self._root_widget, 385 position=( 386 h - icon_size * 0.5 + 25, 387 v 388 + self._button_height * scale 389 - icon_size * 0.24 390 + 1.5, 391 ), 392 transition_delay=self._tdelay, 393 size=(icon_size, icon_size), 394 texture=ba.gettexture('egg1'), 395 tilt_scale=0.0, 396 ) 397 398 ba.containerwidget( 399 edit=self._root_widget, cancel_button=quit_button 400 ) 401 self._tdelay += self._t_delay_inc 402 else: 403 self._quit_button = None 404 405 # If we're not in-game, have no quit button, and this is android, 406 # we want back presses to quit our activity. 407 if ( 408 not self._in_game 409 and not self._have_quit_button 410 and ba.app.platform == 'android' 411 ): 412 413 def _do_quit() -> None: 414 QuitWindow(swish=True, back=True) 415 416 ba.containerwidget( 417 edit=self._root_widget, on_cancel_call=_do_quit 418 ) 419 420 # Add speed-up/slow-down buttons for replays. 421 # (ideally this should be part of a fading-out playback bar like most 422 # media players but this works for now). 423 if ba.internal.is_in_replay(): 424 b_size = 50.0 425 b_buffer = 10.0 426 t_scale = 0.75 427 uiscale = ba.app.ui.uiscale 428 if uiscale is ba.UIScale.SMALL: 429 b_size *= 0.6 430 b_buffer *= 1.0 431 v_offs = -40 432 t_scale = 0.5 433 elif uiscale is ba.UIScale.MEDIUM: 434 v_offs = -70 435 else: 436 v_offs = -100 437 self._replay_speed_text = ba.textwidget( 438 parent=self._root_widget, 439 text=ba.Lstr( 440 resource='watchWindow.playbackSpeedText', 441 subs=[('${SPEED}', str(1.23))], 442 ), 443 position=(h, v + v_offs + 7 * t_scale), 444 h_align='center', 445 v_align='center', 446 size=(0, 0), 447 scale=t_scale, 448 ) 449 450 # Update to current value. 451 self._change_replay_speed(0) 452 453 # Keep updating in a timer in case it gets changed elsewhere. 454 self._change_replay_speed_timer = ba.Timer( 455 0.25, 456 ba.WeakCall(self._change_replay_speed, 0), 457 timetype=ba.TimeType.REAL, 458 repeat=True, 459 ) 460 btn = ba.buttonwidget( 461 parent=self._root_widget, 462 position=( 463 h - b_size - b_buffer, 464 v - b_size - b_buffer + v_offs, 465 ), 466 button_type='square', 467 size=(b_size, b_size), 468 label='', 469 autoselect=True, 470 on_activate_call=ba.Call(self._change_replay_speed, -1), 471 ) 472 ba.textwidget( 473 parent=self._root_widget, 474 draw_controller=btn, 475 text='-', 476 position=( 477 h - b_size * 0.5 - b_buffer, 478 v - b_size * 0.5 - b_buffer + 5 * t_scale + v_offs, 479 ), 480 h_align='center', 481 v_align='center', 482 size=(0, 0), 483 scale=3.0 * t_scale, 484 ) 485 btn = ba.buttonwidget( 486 parent=self._root_widget, 487 position=(h + b_buffer, v - b_size - b_buffer + v_offs), 488 button_type='square', 489 size=(b_size, b_size), 490 label='', 491 autoselect=True, 492 on_activate_call=ba.Call(self._change_replay_speed, 1), 493 ) 494 ba.textwidget( 495 parent=self._root_widget, 496 draw_controller=btn, 497 text='+', 498 position=( 499 h + b_size * 0.5 + b_buffer, 500 v - b_size * 0.5 - b_buffer + 5 * t_scale + v_offs, 501 ), 502 h_align='center', 503 v_align='center', 504 size=(0, 0), 505 scale=3.0 * t_scale, 506 ) 507 508 def _refresh_not_in_game( 509 self, positions: list[tuple[float, float, float]] 510 ) -> tuple[float, float, float]: 511 # pylint: disable=too-many-branches 512 # pylint: disable=too-many-locals 513 # pylint: disable=too-many-statements 514 if not ba.app.did_menu_intro: 515 self._tdelay = 2.0 516 self._t_delay_inc = 0.02 517 self._t_delay_play = 1.7 518 519 def _set_allow_time() -> None: 520 self._next_refresh_allow_time = ba.time(ba.TimeType.REAL) + 2.5 521 522 # Slight hack: widget transitions currently only progress when 523 # frames are being drawn, but this tends to get called before 524 # frame drawing even starts, meaning we don't know exactly how 525 # long we should wait before refreshing to avoid interrupting 526 # the transition. To make things a bit better, let's do a 527 # redundant set of the time in a deferred call which hopefully 528 # happens closer to actual frame draw times. 529 _set_allow_time() 530 ba.pushcall(_set_allow_time) 531 532 ba.app.did_menu_intro = True 533 self._width = 400.0 534 self._height = 200.0 535 enable_account_button = True 536 account_type_name: str | ba.Lstr 537 if ba.internal.get_v1_account_state() == 'signed_in': 538 account_type_name = ba.internal.get_v1_account_display_string() 539 account_type_icon = None 540 account_textcolor = (1.0, 1.0, 1.0) 541 else: 542 account_type_name = ba.Lstr( 543 resource='notSignedInText', 544 fallback_resource='accountSettingsWindow.titleText', 545 ) 546 account_type_icon = None 547 account_textcolor = (1.0, 0.2, 0.2) 548 account_type_icon_color = (1.0, 1.0, 1.0) 549 account_type_call = self._show_account_window 550 account_type_enable_button_sound = True 551 b_count = 3 # play, help, credits 552 if self._have_settings_button: 553 b_count += 1 554 if enable_account_button: 555 b_count += 1 556 if self._have_quit_button: 557 b_count += 1 558 if self._have_store_button: 559 b_count += 1 560 uiscale = ba.app.ui.uiscale 561 if uiscale is ba.UIScale.SMALL: 562 root_widget_scale = 1.6 563 play_button_width = self._button_width * 0.65 564 play_button_height = self._button_height * 1.1 565 small_button_scale = 0.51 if b_count > 6 else 0.63 566 button_y_offs = -20.0 567 button_y_offs2 = -60.0 568 self._button_height *= 1.3 569 button_spacing = 1.04 570 elif uiscale is ba.UIScale.MEDIUM: 571 root_widget_scale = 1.3 572 play_button_width = self._button_width * 0.65 573 play_button_height = self._button_height * 1.1 574 small_button_scale = 0.6 575 button_y_offs = -55.0 576 button_y_offs2 = -75.0 577 self._button_height *= 1.25 578 button_spacing = 1.1 579 else: 580 root_widget_scale = 1.0 581 play_button_width = self._button_width * 0.65 582 play_button_height = self._button_height * 1.1 583 small_button_scale = 0.75 584 button_y_offs = -80.0 585 button_y_offs2 = -100.0 586 self._button_height *= 1.2 587 button_spacing = 1.1 588 spc = self._button_width * small_button_scale * button_spacing 589 ba.containerwidget( 590 edit=self._root_widget, 591 size=(self._width, self._height), 592 background=False, 593 scale=root_widget_scale, 594 ) 595 assert not positions 596 positions.append((self._width * 0.5, button_y_offs, 1.7)) 597 x_offs = self._width * 0.5 - (spc * (b_count - 1) * 0.5) + (spc * 0.5) 598 for i in range(b_count - 1): 599 positions.append( 600 ( 601 x_offs + spc * i - 1.0, 602 button_y_offs + button_y_offs2, 603 small_button_scale, 604 ) 605 ) 606 # In kiosk mode, provide a button to get back to the kiosk menu. 607 if ba.app.demo_mode or ba.app.arcade_mode: 608 h, v, scale = positions[self._p_index] 609 this_b_width = self._button_width * 0.4 * scale 610 demo_menu_delay = ( 611 0.0 612 if self._t_delay_play == 0.0 613 else max(0, self._t_delay_play + 0.1) 614 ) 615 self._demo_menu_button = ba.buttonwidget( 616 parent=self._root_widget, 617 position=(self._width * 0.5 - this_b_width * 0.5, v + 90), 618 size=(this_b_width, 45), 619 autoselect=True, 620 color=(0.45, 0.55, 0.45), 621 textcolor=(0.7, 0.8, 0.7), 622 label=ba.Lstr( 623 resource='modeArcadeText' 624 if ba.app.arcade_mode 625 else 'modeDemoText' 626 ), 627 transition_delay=demo_menu_delay, 628 on_activate_call=self._demo_menu_press, 629 ) 630 else: 631 self._demo_menu_button = None 632 uiscale = ba.app.ui.uiscale 633 foof = ( 634 -1 635 if uiscale is ba.UIScale.SMALL 636 else 1 637 if uiscale is ba.UIScale.MEDIUM 638 else 3 639 ) 640 h, v, scale = positions[self._p_index] 641 v = v + foof 642 gather_delay = ( 643 0.0 644 if self._t_delay_play == 0.0 645 else max(0.0, self._t_delay_play + 0.1) 646 ) 647 assert play_button_width is not None 648 assert play_button_height is not None 649 this_h = h - play_button_width * 0.5 * scale - 40 * scale 650 this_b_width = self._button_width * 0.25 * scale 651 this_b_height = self._button_height * 0.82 * scale 652 self._gather_button = btn = ba.buttonwidget( 653 parent=self._root_widget, 654 position=(this_h - this_b_width * 0.5, v), 655 size=(this_b_width, this_b_height), 656 autoselect=self._use_autoselect, 657 button_type='square', 658 label='', 659 transition_delay=gather_delay, 660 on_activate_call=self._gather_press, 661 ) 662 ba.textwidget( 663 parent=self._root_widget, 664 position=(this_h, v + self._button_height * 0.33), 665 size=(0, 0), 666 scale=0.75, 667 transition_delay=gather_delay, 668 draw_controller=btn, 669 color=(0.75, 1.0, 0.7), 670 maxwidth=self._button_width * 0.33, 671 text=ba.Lstr(resource='gatherWindow.titleText'), 672 h_align='center', 673 v_align='center', 674 ) 675 icon_size = this_b_width * 0.6 676 ba.imagewidget( 677 parent=self._root_widget, 678 size=(icon_size, icon_size), 679 draw_controller=btn, 680 transition_delay=gather_delay, 681 position=(this_h - 0.5 * icon_size, v + 0.31 * this_b_height), 682 texture=ba.gettexture('usersButton'), 683 ) 684 685 # Play button. 686 h, v, scale = positions[self._p_index] 687 self._p_index += 1 688 self._start_button = start_button = ba.buttonwidget( 689 parent=self._root_widget, 690 position=(h - play_button_width * 0.5 * scale, v), 691 size=(play_button_width, play_button_height), 692 autoselect=self._use_autoselect, 693 scale=scale, 694 text_res_scale=2.0, 695 label=ba.Lstr(resource='playText'), 696 transition_delay=self._t_delay_play, 697 on_activate_call=self._play_press, 698 ) 699 ba.containerwidget( 700 edit=self._root_widget, 701 start_button=start_button, 702 selected_child=start_button, 703 ) 704 v = v + foof 705 watch_delay = ( 706 0.0 707 if self._t_delay_play == 0.0 708 else max(0.0, self._t_delay_play - 0.1) 709 ) 710 this_h = h + play_button_width * 0.5 * scale + 40 * scale 711 this_b_width = self._button_width * 0.25 * scale 712 this_b_height = self._button_height * 0.82 * scale 713 self._watch_button = btn = ba.buttonwidget( 714 parent=self._root_widget, 715 position=(this_h - this_b_width * 0.5, v), 716 size=(this_b_width, this_b_height), 717 autoselect=self._use_autoselect, 718 button_type='square', 719 label='', 720 transition_delay=watch_delay, 721 on_activate_call=self._watch_press, 722 ) 723 ba.textwidget( 724 parent=self._root_widget, 725 position=(this_h, v + self._button_height * 0.33), 726 size=(0, 0), 727 scale=0.75, 728 transition_delay=watch_delay, 729 color=(0.75, 1.0, 0.7), 730 draw_controller=btn, 731 maxwidth=self._button_width * 0.33, 732 text=ba.Lstr(resource='watchWindow.titleText'), 733 h_align='center', 734 v_align='center', 735 ) 736 icon_size = this_b_width * 0.55 737 ba.imagewidget( 738 parent=self._root_widget, 739 size=(icon_size, icon_size), 740 draw_controller=btn, 741 transition_delay=watch_delay, 742 position=(this_h - 0.5 * icon_size, v + 0.33 * this_b_height), 743 texture=ba.gettexture('tv'), 744 ) 745 if not self._in_game and enable_account_button: 746 this_b_width = self._button_width 747 h, v, scale = positions[self._p_index] 748 self._p_index += 1 749 self._account_button = ba.buttonwidget( 750 parent=self._root_widget, 751 position=(h - this_b_width * 0.5 * scale, v), 752 size=(this_b_width, self._button_height), 753 scale=scale, 754 label=account_type_name, 755 autoselect=self._use_autoselect, 756 on_activate_call=account_type_call, 757 textcolor=account_textcolor, 758 icon=account_type_icon, 759 icon_color=account_type_icon_color, 760 transition_delay=self._tdelay, 761 enable_sound=account_type_enable_button_sound, 762 ) 763 764 # Scattered eggs on easter. 765 if ( 766 ba.internal.get_v1_account_misc_read_val('easter', False) 767 and not self._in_game 768 ): 769 icon_size = 32 770 ba.imagewidget( 771 parent=self._root_widget, 772 position=( 773 h - icon_size * 0.5 + 35, 774 v 775 + self._button_height * scale 776 - icon_size * 0.24 777 + 1.5, 778 ), 779 transition_delay=self._tdelay, 780 size=(icon_size, icon_size), 781 texture=ba.gettexture('egg2'), 782 tilt_scale=0.0, 783 ) 784 self._tdelay += self._t_delay_inc 785 else: 786 self._account_button = None 787 788 # How-to-play button. 789 h, v, scale = positions[self._p_index] 790 self._p_index += 1 791 btn = ba.buttonwidget( 792 parent=self._root_widget, 793 position=(h - self._button_width * 0.5 * scale, v), 794 scale=scale, 795 autoselect=self._use_autoselect, 796 size=(self._button_width, self._button_height), 797 label=ba.Lstr(resource=self._r + '.howToPlayText'), 798 transition_delay=self._tdelay, 799 on_activate_call=self._howtoplay, 800 ) 801 self._how_to_play_button = btn 802 803 # Scattered eggs on easter. 804 if ( 805 ba.internal.get_v1_account_misc_read_val('easter', False) 806 and not self._in_game 807 ): 808 icon_size = 28 809 ba.imagewidget( 810 parent=self._root_widget, 811 position=( 812 h - icon_size * 0.5 + 30, 813 v + self._button_height * scale - icon_size * 0.24 + 1.5, 814 ), 815 transition_delay=self._tdelay, 816 size=(icon_size, icon_size), 817 texture=ba.gettexture('egg4'), 818 tilt_scale=0.0, 819 ) 820 # Credits button. 821 self._tdelay += self._t_delay_inc 822 h, v, scale = positions[self._p_index] 823 self._p_index += 1 824 self._credits_button = ba.buttonwidget( 825 parent=self._root_widget, 826 position=(h - self._button_width * 0.5 * scale, v), 827 size=(self._button_width, self._button_height), 828 autoselect=self._use_autoselect, 829 label=ba.Lstr(resource=self._r + '.creditsText'), 830 scale=scale, 831 transition_delay=self._tdelay, 832 on_activate_call=self._credits, 833 ) 834 self._tdelay += self._t_delay_inc 835 return h, v, scale 836 837 def _refresh_in_game( 838 self, positions: list[tuple[float, float, float]] 839 ) -> tuple[float, float, float]: 840 # pylint: disable=too-many-branches 841 # pylint: disable=too-many-locals 842 # pylint: disable=too-many-statements 843 custom_menu_entries: list[dict[str, Any]] = [] 844 session = ba.internal.get_foreground_host_session() 845 if session is not None: 846 try: 847 custom_menu_entries = session.get_custom_menu_entries() 848 for cme in custom_menu_entries: 849 if ( 850 not isinstance(cme, dict) 851 or 'label' not in cme 852 or not isinstance(cme['label'], (str, ba.Lstr)) 853 or 'call' not in cme 854 or not callable(cme['call']) 855 ): 856 raise ValueError( 857 'invalid custom menu entry: ' + str(cme) 858 ) 859 except Exception: 860 custom_menu_entries = [] 861 ba.print_exception( 862 f'Error getting custom menu entries for {session}' 863 ) 864 self._width = 250.0 865 self._height = 250.0 if self._input_player else 180.0 866 if (self._is_demo or self._is_arcade) and self._input_player: 867 self._height -= 40 868 if not self._have_settings_button: 869 self._height -= 50 870 if self._connected_to_remote_player: 871 # In this case we have a leave *and* a disconnect button. 872 self._height += 50 873 self._height += 50 * (len(custom_menu_entries)) 874 uiscale = ba.app.ui.uiscale 875 ba.containerwidget( 876 edit=self._root_widget, 877 size=(self._width, self._height), 878 scale=( 879 2.15 880 if uiscale is ba.UIScale.SMALL 881 else 1.6 882 if uiscale is ba.UIScale.MEDIUM 883 else 1.0 884 ), 885 ) 886 h = 125.0 887 v = self._height - 80.0 if self._input_player else self._height - 60 888 h_offset = 0 889 d_h_offset = 0 890 v_offset = -50 891 for _i in range(6 + len(custom_menu_entries)): 892 positions.append((h, v, 1.0)) 893 v += v_offset 894 h += h_offset 895 h_offset += d_h_offset 896 self._start_button = None 897 ba.app.pause() 898 899 # Player name if applicable. 900 if self._input_player: 901 player_name = self._input_player.getname() 902 h, v, scale = positions[self._p_index] 903 v += 35 904 ba.textwidget( 905 parent=self._root_widget, 906 position=(h - self._button_width / 2, v), 907 size=(self._button_width, self._button_height), 908 color=(1, 1, 1, 0.5), 909 scale=0.7, 910 h_align='center', 911 text=ba.Lstr(value=player_name), 912 ) 913 else: 914 player_name = '' 915 h, v, scale = positions[self._p_index] 916 self._p_index += 1 917 btn = ba.buttonwidget( 918 parent=self._root_widget, 919 position=(h - self._button_width / 2, v), 920 size=(self._button_width, self._button_height), 921 scale=scale, 922 label=ba.Lstr(resource=self._r + '.resumeText'), 923 autoselect=self._use_autoselect, 924 on_activate_call=self._resume, 925 ) 926 ba.containerwidget(edit=self._root_widget, cancel_button=btn) 927 928 # Add any custom options defined by the current game. 929 for entry in custom_menu_entries: 930 h, v, scale = positions[self._p_index] 931 self._p_index += 1 932 933 # Ask the entry whether we should resume when we call 934 # it (defaults to true). 935 resume = bool(entry.get('resume_on_call', True)) 936 937 if resume: 938 call = ba.Call(self._resume_and_call, entry['call']) 939 else: 940 call = ba.Call(entry['call'], ba.WeakCall(self._resume)) 941 942 ba.buttonwidget( 943 parent=self._root_widget, 944 position=(h - self._button_width / 2, v), 945 size=(self._button_width, self._button_height), 946 scale=scale, 947 on_activate_call=call, 948 label=entry['label'], 949 autoselect=self._use_autoselect, 950 ) 951 # Add a 'leave' button if the menu-owner has a player. 952 if (self._input_player or self._connected_to_remote_player) and not ( 953 self._is_demo or self._is_arcade 954 ): 955 h, v, scale = positions[self._p_index] 956 self._p_index += 1 957 btn = ba.buttonwidget( 958 parent=self._root_widget, 959 position=(h - self._button_width / 2, v), 960 size=(self._button_width, self._button_height), 961 scale=scale, 962 on_activate_call=self._leave, 963 label='', 964 autoselect=self._use_autoselect, 965 ) 966 967 if ( 968 player_name != '' 969 and player_name[0] != '<' 970 and player_name[-1] != '>' 971 ): 972 txt = ba.Lstr( 973 resource=self._r + '.justPlayerText', 974 subs=[('${NAME}', player_name)], 975 ) 976 else: 977 txt = ba.Lstr(value=player_name) 978 ba.textwidget( 979 parent=self._root_widget, 980 position=( 981 h, 982 v 983 + self._button_height 984 * (0.64 if player_name != '' else 0.5), 985 ), 986 size=(0, 0), 987 text=ba.Lstr(resource=self._r + '.leaveGameText'), 988 scale=(0.83 if player_name != '' else 1.0), 989 color=(0.75, 1.0, 0.7), 990 h_align='center', 991 v_align='center', 992 draw_controller=btn, 993 maxwidth=self._button_width * 0.9, 994 ) 995 ba.textwidget( 996 parent=self._root_widget, 997 position=(h, v + self._button_height * 0.27), 998 size=(0, 0), 999 text=txt, 1000 color=(0.75, 1.0, 0.7), 1001 h_align='center', 1002 v_align='center', 1003 draw_controller=btn, 1004 scale=0.45, 1005 maxwidth=self._button_width * 0.9, 1006 ) 1007 return h, v, scale 1008 1009 def _change_replay_speed(self, offs: int) -> None: 1010 if not self._replay_speed_text: 1011 if ba.do_once(): 1012 print('_change_replay_speed called without widget') 1013 return 1014 ba.internal.set_replay_speed_exponent( 1015 ba.internal.get_replay_speed_exponent() + offs 1016 ) 1017 actual_speed = pow(2.0, ba.internal.get_replay_speed_exponent()) 1018 ba.textwidget( 1019 edit=self._replay_speed_text, 1020 text=ba.Lstr( 1021 resource='watchWindow.playbackSpeedText', 1022 subs=[('${SPEED}', str(actual_speed))], 1023 ), 1024 ) 1025 1026 def _quit(self) -> None: 1027 # pylint: disable=cyclic-import 1028 from bastd.ui.confirm import QuitWindow 1029 1030 QuitWindow(origin_widget=self._quit_button) 1031 1032 def _demo_menu_press(self) -> None: 1033 # pylint: disable=cyclic-import 1034 from bastd.ui.kiosk import KioskWindow 1035 1036 self._save_state() 1037 ba.containerwidget(edit=self._root_widget, transition='out_right') 1038 ba.app.ui.set_main_menu_window( 1039 KioskWindow(transition='in_left').get_root_widget() 1040 ) 1041 1042 def _show_account_window(self) -> None: 1043 # pylint: disable=cyclic-import 1044 from bastd.ui.account.settings import AccountSettingsWindow 1045 1046 self._save_state() 1047 ba.containerwidget(edit=self._root_widget, transition='out_left') 1048 ba.app.ui.set_main_menu_window( 1049 AccountSettingsWindow( 1050 origin_widget=self._account_button 1051 ).get_root_widget() 1052 ) 1053 1054 def _on_store_pressed(self) -> None: 1055 # pylint: disable=cyclic-import 1056 from bastd.ui.store.browser import StoreBrowserWindow 1057 from bastd.ui.account import show_sign_in_prompt 1058 1059 if ba.internal.get_v1_account_state() != 'signed_in': 1060 show_sign_in_prompt() 1061 return 1062 self._save_state() 1063 ba.containerwidget(edit=self._root_widget, transition='out_left') 1064 ba.app.ui.set_main_menu_window( 1065 StoreBrowserWindow( 1066 origin_widget=self._store_button 1067 ).get_root_widget() 1068 ) 1069 1070 def _is_benchmark(self) -> bool: 1071 session = ba.internal.get_foreground_host_session() 1072 return ( 1073 getattr(session, 'benchmark_type', None) == 'cpu' 1074 or ba.app.stress_test_reset_timer is not None 1075 ) 1076 1077 def _confirm_end_game(self) -> None: 1078 # pylint: disable=cyclic-import 1079 from bastd.ui.confirm import ConfirmWindow 1080 1081 # FIXME: Currently we crash calling this on client-sessions. 1082 1083 # Select cancel by default; this occasionally gets called by accident 1084 # in a fit of button mashing and this will help reduce damage. 1085 ConfirmWindow( 1086 ba.Lstr(resource=self._r + '.exitToMenuText'), 1087 self._end_game, 1088 cancel_is_selected=True, 1089 ) 1090 1091 def _confirm_end_test(self) -> None: 1092 # pylint: disable=cyclic-import 1093 from bastd.ui.confirm import ConfirmWindow 1094 1095 # Select cancel by default; this occasionally gets called by accident 1096 # in a fit of button mashing and this will help reduce damage. 1097 ConfirmWindow( 1098 ba.Lstr(resource=self._r + '.exitToMenuText'), 1099 self._end_game, 1100 cancel_is_selected=True, 1101 ) 1102 1103 def _confirm_end_replay(self) -> None: 1104 # pylint: disable=cyclic-import 1105 from bastd.ui.confirm import ConfirmWindow 1106 1107 # Select cancel by default; this occasionally gets called by accident 1108 # in a fit of button mashing and this will help reduce damage. 1109 ConfirmWindow( 1110 ba.Lstr(resource=self._r + '.exitToMenuText'), 1111 self._end_game, 1112 cancel_is_selected=True, 1113 ) 1114 1115 def _confirm_leave_party(self) -> None: 1116 # pylint: disable=cyclic-import 1117 from bastd.ui.confirm import ConfirmWindow 1118 1119 # Select cancel by default; this occasionally gets called by accident 1120 # in a fit of button mashing and this will help reduce damage. 1121 ConfirmWindow( 1122 ba.Lstr(resource=self._r + '.leavePartyConfirmText'), 1123 self._leave_party, 1124 cancel_is_selected=True, 1125 ) 1126 1127 def _leave_party(self) -> None: 1128 ba.internal.disconnect_from_host() 1129 1130 def _end_game(self) -> None: 1131 if not self._root_widget: 1132 return 1133 ba.containerwidget(edit=self._root_widget, transition='out_left') 1134 ba.app.return_to_main_menu_session_gracefully(reset_ui=False) 1135 1136 def _leave(self) -> None: 1137 if self._input_player: 1138 self._input_player.remove_from_game() 1139 elif self._connected_to_remote_player: 1140 if self._input_device: 1141 self._input_device.remove_remote_player_from_game() 1142 self._resume() 1143 1144 def _credits(self) -> None: 1145 # pylint: disable=cyclic-import 1146 from bastd.ui.creditslist import CreditsListWindow 1147 1148 self._save_state() 1149 ba.containerwidget(edit=self._root_widget, transition='out_left') 1150 ba.app.ui.set_main_menu_window( 1151 CreditsListWindow( 1152 origin_widget=self._credits_button 1153 ).get_root_widget() 1154 ) 1155 1156 def _howtoplay(self) -> None: 1157 # pylint: disable=cyclic-import 1158 from bastd.ui.helpui import HelpWindow 1159 1160 self._save_state() 1161 ba.containerwidget(edit=self._root_widget, transition='out_left') 1162 ba.app.ui.set_main_menu_window( 1163 HelpWindow( 1164 main_menu=True, origin_widget=self._how_to_play_button 1165 ).get_root_widget() 1166 ) 1167 1168 def _settings(self) -> None: 1169 # pylint: disable=cyclic-import 1170 from bastd.ui.settings.allsettings import AllSettingsWindow 1171 1172 self._save_state() 1173 ba.containerwidget(edit=self._root_widget, transition='out_left') 1174 ba.app.ui.set_main_menu_window( 1175 AllSettingsWindow( 1176 origin_widget=self._settings_button 1177 ).get_root_widget() 1178 ) 1179 1180 def _resume_and_call(self, call: Callable[[], Any]) -> None: 1181 self._resume() 1182 call() 1183 1184 def _do_game_service_press(self) -> None: 1185 self._save_state() 1186 ba.internal.show_online_score_ui() 1187 1188 def _save_state(self) -> None: 1189 1190 # Don't do this for the in-game menu. 1191 if self._in_game: 1192 return 1193 sel = self._root_widget.get_selected_child() 1194 if sel == self._start_button: 1195 ba.app.ui.main_menu_selection = 'Start' 1196 elif sel == self._gather_button: 1197 ba.app.ui.main_menu_selection = 'Gather' 1198 elif sel == self._watch_button: 1199 ba.app.ui.main_menu_selection = 'Watch' 1200 elif sel == self._how_to_play_button: 1201 ba.app.ui.main_menu_selection = 'HowToPlay' 1202 elif sel == self._credits_button: 1203 ba.app.ui.main_menu_selection = 'Credits' 1204 elif sel == self._settings_button: 1205 ba.app.ui.main_menu_selection = 'Settings' 1206 elif sel == self._account_button: 1207 ba.app.ui.main_menu_selection = 'Account' 1208 elif sel == self._store_button: 1209 ba.app.ui.main_menu_selection = 'Store' 1210 elif sel == self._quit_button: 1211 ba.app.ui.main_menu_selection = 'Quit' 1212 elif sel == self._demo_menu_button: 1213 ba.app.ui.main_menu_selection = 'DemoMenu' 1214 else: 1215 print('unknown widget in main menu store selection:', sel) 1216 ba.app.ui.main_menu_selection = 'Start' 1217 1218 def _restore_state(self) -> None: 1219 # pylint: disable=too-many-branches 1220 1221 # Don't do this for the in-game menu. 1222 if self._in_game: 1223 return 1224 sel_name = ba.app.ui.main_menu_selection 1225 sel: ba.Widget | None 1226 if sel_name is None: 1227 sel_name = 'Start' 1228 if sel_name == 'HowToPlay': 1229 sel = self._how_to_play_button 1230 elif sel_name == 'Gather': 1231 sel = self._gather_button 1232 elif sel_name == 'Watch': 1233 sel = self._watch_button 1234 elif sel_name == 'Credits': 1235 sel = self._credits_button 1236 elif sel_name == 'Settings': 1237 sel = self._settings_button 1238 elif sel_name == 'Account': 1239 sel = self._account_button 1240 elif sel_name == 'Store': 1241 sel = self._store_button 1242 elif sel_name == 'Quit': 1243 sel = self._quit_button 1244 elif sel_name == 'DemoMenu': 1245 sel = self._demo_menu_button 1246 else: 1247 sel = self._start_button 1248 if sel is not None: 1249 ba.containerwidget(edit=self._root_widget, selected_child=sel) 1250 1251 def _gather_press(self) -> None: 1252 # pylint: disable=cyclic-import 1253 from bastd.ui.gather import GatherWindow 1254 1255 self._save_state() 1256 ba.containerwidget(edit=self._root_widget, transition='out_left') 1257 ba.app.ui.set_main_menu_window( 1258 GatherWindow(origin_widget=self._gather_button).get_root_widget() 1259 ) 1260 1261 def _watch_press(self) -> None: 1262 # pylint: disable=cyclic-import 1263 from bastd.ui.watch import WatchWindow 1264 1265 self._save_state() 1266 ba.containerwidget(edit=self._root_widget, transition='out_left') 1267 ba.app.ui.set_main_menu_window( 1268 WatchWindow(origin_widget=self._watch_button).get_root_widget() 1269 ) 1270 1271 def _play_press(self) -> None: 1272 # pylint: disable=cyclic-import 1273 from bastd.ui.play import PlayWindow 1274 1275 self._save_state() 1276 ba.containerwidget(edit=self._root_widget, transition='out_left') 1277 1278 ba.app.ui.selecting_private_party_playlist = False 1279 ba.app.ui.set_main_menu_window( 1280 PlayWindow(origin_widget=self._start_button).get_root_widget() 1281 ) 1282 1283 def _resume(self) -> None: 1284 ba.app.resume() 1285 if self._root_widget: 1286 ba.containerwidget(edit=self._root_widget, transition='out_right') 1287 ba.app.ui.clear_main_menu_window() 1288 1289 # If there's callbacks waiting for this window to go away, call them. 1290 for call in ba.app.main_menu_resume_callbacks: 1291 call() 1292 del ba.app.main_menu_resume_callbacks[:]
class
MainMenuWindow(ba.ui.Window):
18class MainMenuWindow(ba.Window): 19 """The main menu window, both in-game and in the main menu session.""" 20 21 def __init__(self, transition: str | None = 'in_right'): 22 # pylint: disable=cyclic-import 23 import threading 24 from bastd.mainmenu import MainMenuSession 25 26 self._in_game = not isinstance( 27 ba.internal.get_foreground_host_session(), MainMenuSession 28 ) 29 30 # Preload some modules we use in a background thread so we won't 31 # have a visual hitch when the user taps them. 32 threading.Thread(target=self._preload_modules).start() 33 34 if not self._in_game: 35 ba.set_analytics_screen('Main Menu') 36 self._show_remote_app_info_on_first_launch() 37 38 # Make a vanilla container; we'll modify it to our needs in refresh. 39 super().__init__( 40 root_widget=ba.containerwidget( 41 transition=transition, 42 toolbar_visibility='menu_minimal_no_back' 43 if self._in_game 44 else 'menu_minimal_no_back', 45 ) 46 ) 47 48 # Grab this stuff in case it changes. 49 self._is_demo = ba.app.demo_mode 50 self._is_arcade = ba.app.arcade_mode 51 self._is_iircade = ba.app.iircade_mode 52 53 self._tdelay = 0.0 54 self._t_delay_inc = 0.02 55 self._t_delay_play = 1.7 56 self._p_index = 0 57 self._use_autoselect = True 58 self._button_width = 200.0 59 self._button_height = 45.0 60 self._width = 100.0 61 self._height = 100.0 62 self._demo_menu_button: ba.Widget | None = None 63 self._gather_button: ba.Widget | None = None 64 self._start_button: ba.Widget | None = None 65 self._watch_button: ba.Widget | None = None 66 self._account_button: ba.Widget | None = None 67 self._how_to_play_button: ba.Widget | None = None 68 self._credits_button: ba.Widget | None = None 69 self._settings_button: ba.Widget | None = None 70 self._next_refresh_allow_time = 0.0 71 72 self._store_char_tex = self._get_store_char_tex() 73 74 self._refresh() 75 self._restore_state() 76 77 # Keep an eye on a few things and refresh if they change. 78 self._account_state = ba.internal.get_v1_account_state() 79 self._account_state_num = ba.internal.get_v1_account_state_num() 80 self._account_type = ( 81 ba.internal.get_v1_account_type() 82 if self._account_state == 'signed_in' 83 else None 84 ) 85 self._refresh_timer = ba.Timer( 86 0.27, 87 ba.WeakCall(self._check_refresh), 88 repeat=True, 89 timetype=ba.TimeType.REAL, 90 ) 91 92 # noinspection PyUnresolvedReferences 93 @staticmethod 94 def _preload_modules() -> None: 95 """Preload modules we use (called in bg thread).""" 96 import bastd.ui.getremote as _unused 97 import bastd.ui.confirm as _unused2 98 import bastd.ui.store.button as _unused3 99 import bastd.ui.kiosk as _unused4 100 import bastd.ui.account.settings as _unused5 101 import bastd.ui.store.browser as _unused6 102 import bastd.ui.creditslist as _unused7 103 import bastd.ui.helpui as _unused8 104 import bastd.ui.settings.allsettings as _unused9 105 import bastd.ui.gather as _unused10 106 import bastd.ui.watch as _unused11 107 import bastd.ui.play as _unused12 108 109 def _show_remote_app_info_on_first_launch(self) -> None: 110 # The first time the non-in-game menu pops up, we might wanna show 111 # a 'get-remote-app' dialog in front of it. 112 if ba.app.first_main_menu: 113 ba.app.first_main_menu = False 114 try: 115 app = ba.app 116 force_test = False 117 ba.internal.get_local_active_input_devices_count() 118 if ( 119 (app.on_tv or app.platform == 'mac') 120 and ba.app.config.get('launchCount', 0) <= 1 121 ) or force_test: 122 123 def _check_show_bs_remote_window() -> None: 124 try: 125 from bastd.ui.getremote import GetBSRemoteWindow 126 127 ba.playsound(ba.getsound('swish')) 128 GetBSRemoteWindow() 129 except Exception: 130 ba.print_exception( 131 'Error showing get-remote window.' 132 ) 133 134 ba.timer( 135 2.5, 136 _check_show_bs_remote_window, 137 timetype=ba.TimeType.REAL, 138 ) 139 except Exception: 140 ba.print_exception('Error showing get-remote-app info') 141 142 def _get_store_char_tex(self) -> str: 143 return ( 144 'storeCharacterXmas' 145 if ba.internal.get_v1_account_misc_read_val('xmas', False) 146 else 'storeCharacterEaster' 147 if ba.internal.get_v1_account_misc_read_val('easter', False) 148 else 'storeCharacter' 149 ) 150 151 def _check_refresh(self) -> None: 152 if not self._root_widget: 153 return 154 155 now = ba.time(ba.TimeType.REAL) 156 if now < self._next_refresh_allow_time: 157 return 158 159 # Don't refresh for the first few seconds the game is up so we don't 160 # interrupt the transition in. 161 # ba.app.main_menu_window_refresh_check_count += 1 162 # if ba.app.main_menu_window_refresh_check_count < 4: 163 # return 164 165 store_char_tex = self._get_store_char_tex() 166 account_state_num = ba.internal.get_v1_account_state_num() 167 if ( 168 account_state_num != self._account_state_num 169 or store_char_tex != self._store_char_tex 170 ): 171 self._store_char_tex = store_char_tex 172 self._account_state_num = account_state_num 173 account_state = ( 174 self._account_state 175 ) = ba.internal.get_v1_account_state() 176 self._account_type = ( 177 ba.internal.get_v1_account_type() 178 if account_state == 'signed_in' 179 else None 180 ) 181 self._save_state() 182 self._refresh() 183 self._restore_state() 184 185 def get_play_button(self) -> ba.Widget | None: 186 """Return the play button.""" 187 return self._start_button 188 189 def _refresh(self) -> None: 190 # pylint: disable=too-many-branches 191 # pylint: disable=too-many-locals 192 # pylint: disable=too-many-statements 193 from bastd.ui.confirm import QuitWindow 194 from bastd.ui.store.button import StoreButton 195 196 # Clear everything that was there. 197 children = self._root_widget.get_children() 198 for child in children: 199 child.delete() 200 201 self._tdelay = 0.0 202 self._t_delay_inc = 0.0 203 self._t_delay_play = 0.0 204 self._button_width = 200.0 205 self._button_height = 45.0 206 207 self._r = 'mainMenu' 208 209 app = ba.app 210 self._have_quit_button = app.ui.uiscale is ba.UIScale.LARGE or ( 211 app.platform == 'windows' and app.subplatform == 'oculus' 212 ) 213 214 self._have_store_button = not self._in_game 215 216 self._have_settings_button = ( 217 not self._in_game or not app.toolbar_test 218 ) and not (self._is_demo or self._is_arcade or self._is_iircade) 219 220 self._input_device = input_device = ba.internal.get_ui_input_device() 221 self._input_player = input_device.player if input_device else None 222 self._connected_to_remote_player = ( 223 input_device.is_connected_to_remote_player() 224 if input_device 225 else False 226 ) 227 228 positions: list[tuple[float, float, float]] = [] 229 self._p_index = 0 230 231 if self._in_game: 232 h, v, scale = self._refresh_in_game(positions) 233 else: 234 h, v, scale = self._refresh_not_in_game(positions) 235 236 if self._have_settings_button: 237 h, v, scale = positions[self._p_index] 238 self._p_index += 1 239 self._settings_button = ba.buttonwidget( 240 parent=self._root_widget, 241 position=(h - self._button_width * 0.5 * scale, v), 242 size=(self._button_width, self._button_height), 243 scale=scale, 244 autoselect=self._use_autoselect, 245 label=ba.Lstr(resource=self._r + '.settingsText'), 246 transition_delay=self._tdelay, 247 on_activate_call=self._settings, 248 ) 249 250 # Scattered eggs on easter. 251 if ( 252 ba.internal.get_v1_account_misc_read_val('easter', False) 253 and not self._in_game 254 ): 255 icon_size = 34 256 ba.imagewidget( 257 parent=self._root_widget, 258 position=( 259 h - icon_size * 0.5 - 15, 260 v + self._button_height * scale - icon_size * 0.24 + 1.5, 261 ), 262 transition_delay=self._tdelay, 263 size=(icon_size, icon_size), 264 texture=ba.gettexture('egg3'), 265 tilt_scale=0.0, 266 ) 267 268 self._tdelay += self._t_delay_inc 269 270 if self._in_game: 271 h, v, scale = positions[self._p_index] 272 self._p_index += 1 273 274 # If we're in a replay, we have a 'Leave Replay' button. 275 if ba.internal.is_in_replay(): 276 ba.buttonwidget( 277 parent=self._root_widget, 278 position=(h - self._button_width * 0.5 * scale, v), 279 scale=scale, 280 size=(self._button_width, self._button_height), 281 autoselect=self._use_autoselect, 282 label=ba.Lstr(resource='replayEndText'), 283 on_activate_call=self._confirm_end_replay, 284 ) 285 elif ba.internal.get_foreground_host_session() is not None: 286 ba.buttonwidget( 287 parent=self._root_widget, 288 position=(h - self._button_width * 0.5 * scale, v), 289 scale=scale, 290 size=(self._button_width, self._button_height), 291 autoselect=self._use_autoselect, 292 label=ba.Lstr( 293 resource=self._r 294 + ( 295 '.endTestText' 296 if self._is_benchmark() 297 else '.endGameText' 298 ) 299 ), 300 on_activate_call=( 301 self._confirm_end_test 302 if self._is_benchmark() 303 else self._confirm_end_game 304 ), 305 ) 306 # Assume we're in a client-session. 307 else: 308 ba.buttonwidget( 309 parent=self._root_widget, 310 position=(h - self._button_width * 0.5 * scale, v), 311 scale=scale, 312 size=(self._button_width, self._button_height), 313 autoselect=self._use_autoselect, 314 label=ba.Lstr(resource=self._r + '.leavePartyText'), 315 on_activate_call=self._confirm_leave_party, 316 ) 317 318 self._store_button: ba.Widget | None 319 if self._have_store_button: 320 this_b_width = self._button_width 321 h, v, scale = positions[self._p_index] 322 self._p_index += 1 323 324 sbtn = self._store_button_instance = StoreButton( 325 parent=self._root_widget, 326 position=(h - this_b_width * 0.5 * scale, v), 327 size=(this_b_width, self._button_height), 328 scale=scale, 329 on_activate_call=ba.WeakCall(self._on_store_pressed), 330 sale_scale=1.3, 331 transition_delay=self._tdelay, 332 ) 333 self._store_button = store_button = sbtn.get_button() 334 uiscale = ba.app.ui.uiscale 335 icon_size = ( 336 55 337 if uiscale is ba.UIScale.SMALL 338 else 55 339 if uiscale is ba.UIScale.MEDIUM 340 else 70 341 ) 342 ba.imagewidget( 343 parent=self._root_widget, 344 position=( 345 h - icon_size * 0.5, 346 v + self._button_height * scale - icon_size * 0.23, 347 ), 348 transition_delay=self._tdelay, 349 size=(icon_size, icon_size), 350 texture=ba.gettexture(self._store_char_tex), 351 tilt_scale=0.0, 352 draw_controller=store_button, 353 ) 354 355 self._tdelay += self._t_delay_inc 356 else: 357 self._store_button = None 358 359 self._quit_button: ba.Widget | None 360 if not self._in_game and self._have_quit_button: 361 h, v, scale = positions[self._p_index] 362 self._p_index += 1 363 self._quit_button = quit_button = ba.buttonwidget( 364 parent=self._root_widget, 365 autoselect=self._use_autoselect, 366 position=(h - self._button_width * 0.5 * scale, v), 367 size=(self._button_width, self._button_height), 368 scale=scale, 369 label=ba.Lstr( 370 resource=self._r 371 + ( 372 '.quitText' 373 if 'Mac' in ba.app.user_agent_string 374 else '.exitGameText' 375 ) 376 ), 377 on_activate_call=self._quit, 378 transition_delay=self._tdelay, 379 ) 380 381 # Scattered eggs on easter. 382 if ba.internal.get_v1_account_misc_read_val('easter', False): 383 icon_size = 30 384 ba.imagewidget( 385 parent=self._root_widget, 386 position=( 387 h - icon_size * 0.5 + 25, 388 v 389 + self._button_height * scale 390 - icon_size * 0.24 391 + 1.5, 392 ), 393 transition_delay=self._tdelay, 394 size=(icon_size, icon_size), 395 texture=ba.gettexture('egg1'), 396 tilt_scale=0.0, 397 ) 398 399 ba.containerwidget( 400 edit=self._root_widget, cancel_button=quit_button 401 ) 402 self._tdelay += self._t_delay_inc 403 else: 404 self._quit_button = None 405 406 # If we're not in-game, have no quit button, and this is android, 407 # we want back presses to quit our activity. 408 if ( 409 not self._in_game 410 and not self._have_quit_button 411 and ba.app.platform == 'android' 412 ): 413 414 def _do_quit() -> None: 415 QuitWindow(swish=True, back=True) 416 417 ba.containerwidget( 418 edit=self._root_widget, on_cancel_call=_do_quit 419 ) 420 421 # Add speed-up/slow-down buttons for replays. 422 # (ideally this should be part of a fading-out playback bar like most 423 # media players but this works for now). 424 if ba.internal.is_in_replay(): 425 b_size = 50.0 426 b_buffer = 10.0 427 t_scale = 0.75 428 uiscale = ba.app.ui.uiscale 429 if uiscale is ba.UIScale.SMALL: 430 b_size *= 0.6 431 b_buffer *= 1.0 432 v_offs = -40 433 t_scale = 0.5 434 elif uiscale is ba.UIScale.MEDIUM: 435 v_offs = -70 436 else: 437 v_offs = -100 438 self._replay_speed_text = ba.textwidget( 439 parent=self._root_widget, 440 text=ba.Lstr( 441 resource='watchWindow.playbackSpeedText', 442 subs=[('${SPEED}', str(1.23))], 443 ), 444 position=(h, v + v_offs + 7 * t_scale), 445 h_align='center', 446 v_align='center', 447 size=(0, 0), 448 scale=t_scale, 449 ) 450 451 # Update to current value. 452 self._change_replay_speed(0) 453 454 # Keep updating in a timer in case it gets changed elsewhere. 455 self._change_replay_speed_timer = ba.Timer( 456 0.25, 457 ba.WeakCall(self._change_replay_speed, 0), 458 timetype=ba.TimeType.REAL, 459 repeat=True, 460 ) 461 btn = ba.buttonwidget( 462 parent=self._root_widget, 463 position=( 464 h - b_size - b_buffer, 465 v - b_size - b_buffer + v_offs, 466 ), 467 button_type='square', 468 size=(b_size, b_size), 469 label='', 470 autoselect=True, 471 on_activate_call=ba.Call(self._change_replay_speed, -1), 472 ) 473 ba.textwidget( 474 parent=self._root_widget, 475 draw_controller=btn, 476 text='-', 477 position=( 478 h - b_size * 0.5 - b_buffer, 479 v - b_size * 0.5 - b_buffer + 5 * t_scale + v_offs, 480 ), 481 h_align='center', 482 v_align='center', 483 size=(0, 0), 484 scale=3.0 * t_scale, 485 ) 486 btn = ba.buttonwidget( 487 parent=self._root_widget, 488 position=(h + b_buffer, v - b_size - b_buffer + v_offs), 489 button_type='square', 490 size=(b_size, b_size), 491 label='', 492 autoselect=True, 493 on_activate_call=ba.Call(self._change_replay_speed, 1), 494 ) 495 ba.textwidget( 496 parent=self._root_widget, 497 draw_controller=btn, 498 text='+', 499 position=( 500 h + b_size * 0.5 + b_buffer, 501 v - b_size * 0.5 - b_buffer + 5 * t_scale + v_offs, 502 ), 503 h_align='center', 504 v_align='center', 505 size=(0, 0), 506 scale=3.0 * t_scale, 507 ) 508 509 def _refresh_not_in_game( 510 self, positions: list[tuple[float, float, float]] 511 ) -> tuple[float, float, float]: 512 # pylint: disable=too-many-branches 513 # pylint: disable=too-many-locals 514 # pylint: disable=too-many-statements 515 if not ba.app.did_menu_intro: 516 self._tdelay = 2.0 517 self._t_delay_inc = 0.02 518 self._t_delay_play = 1.7 519 520 def _set_allow_time() -> None: 521 self._next_refresh_allow_time = ba.time(ba.TimeType.REAL) + 2.5 522 523 # Slight hack: widget transitions currently only progress when 524 # frames are being drawn, but this tends to get called before 525 # frame drawing even starts, meaning we don't know exactly how 526 # long we should wait before refreshing to avoid interrupting 527 # the transition. To make things a bit better, let's do a 528 # redundant set of the time in a deferred call which hopefully 529 # happens closer to actual frame draw times. 530 _set_allow_time() 531 ba.pushcall(_set_allow_time) 532 533 ba.app.did_menu_intro = True 534 self._width = 400.0 535 self._height = 200.0 536 enable_account_button = True 537 account_type_name: str | ba.Lstr 538 if ba.internal.get_v1_account_state() == 'signed_in': 539 account_type_name = ba.internal.get_v1_account_display_string() 540 account_type_icon = None 541 account_textcolor = (1.0, 1.0, 1.0) 542 else: 543 account_type_name = ba.Lstr( 544 resource='notSignedInText', 545 fallback_resource='accountSettingsWindow.titleText', 546 ) 547 account_type_icon = None 548 account_textcolor = (1.0, 0.2, 0.2) 549 account_type_icon_color = (1.0, 1.0, 1.0) 550 account_type_call = self._show_account_window 551 account_type_enable_button_sound = True 552 b_count = 3 # play, help, credits 553 if self._have_settings_button: 554 b_count += 1 555 if enable_account_button: 556 b_count += 1 557 if self._have_quit_button: 558 b_count += 1 559 if self._have_store_button: 560 b_count += 1 561 uiscale = ba.app.ui.uiscale 562 if uiscale is ba.UIScale.SMALL: 563 root_widget_scale = 1.6 564 play_button_width = self._button_width * 0.65 565 play_button_height = self._button_height * 1.1 566 small_button_scale = 0.51 if b_count > 6 else 0.63 567 button_y_offs = -20.0 568 button_y_offs2 = -60.0 569 self._button_height *= 1.3 570 button_spacing = 1.04 571 elif uiscale is ba.UIScale.MEDIUM: 572 root_widget_scale = 1.3 573 play_button_width = self._button_width * 0.65 574 play_button_height = self._button_height * 1.1 575 small_button_scale = 0.6 576 button_y_offs = -55.0 577 button_y_offs2 = -75.0 578 self._button_height *= 1.25 579 button_spacing = 1.1 580 else: 581 root_widget_scale = 1.0 582 play_button_width = self._button_width * 0.65 583 play_button_height = self._button_height * 1.1 584 small_button_scale = 0.75 585 button_y_offs = -80.0 586 button_y_offs2 = -100.0 587 self._button_height *= 1.2 588 button_spacing = 1.1 589 spc = self._button_width * small_button_scale * button_spacing 590 ba.containerwidget( 591 edit=self._root_widget, 592 size=(self._width, self._height), 593 background=False, 594 scale=root_widget_scale, 595 ) 596 assert not positions 597 positions.append((self._width * 0.5, button_y_offs, 1.7)) 598 x_offs = self._width * 0.5 - (spc * (b_count - 1) * 0.5) + (spc * 0.5) 599 for i in range(b_count - 1): 600 positions.append( 601 ( 602 x_offs + spc * i - 1.0, 603 button_y_offs + button_y_offs2, 604 small_button_scale, 605 ) 606 ) 607 # In kiosk mode, provide a button to get back to the kiosk menu. 608 if ba.app.demo_mode or ba.app.arcade_mode: 609 h, v, scale = positions[self._p_index] 610 this_b_width = self._button_width * 0.4 * scale 611 demo_menu_delay = ( 612 0.0 613 if self._t_delay_play == 0.0 614 else max(0, self._t_delay_play + 0.1) 615 ) 616 self._demo_menu_button = ba.buttonwidget( 617 parent=self._root_widget, 618 position=(self._width * 0.5 - this_b_width * 0.5, v + 90), 619 size=(this_b_width, 45), 620 autoselect=True, 621 color=(0.45, 0.55, 0.45), 622 textcolor=(0.7, 0.8, 0.7), 623 label=ba.Lstr( 624 resource='modeArcadeText' 625 if ba.app.arcade_mode 626 else 'modeDemoText' 627 ), 628 transition_delay=demo_menu_delay, 629 on_activate_call=self._demo_menu_press, 630 ) 631 else: 632 self._demo_menu_button = None 633 uiscale = ba.app.ui.uiscale 634 foof = ( 635 -1 636 if uiscale is ba.UIScale.SMALL 637 else 1 638 if uiscale is ba.UIScale.MEDIUM 639 else 3 640 ) 641 h, v, scale = positions[self._p_index] 642 v = v + foof 643 gather_delay = ( 644 0.0 645 if self._t_delay_play == 0.0 646 else max(0.0, self._t_delay_play + 0.1) 647 ) 648 assert play_button_width is not None 649 assert play_button_height is not None 650 this_h = h - play_button_width * 0.5 * scale - 40 * scale 651 this_b_width = self._button_width * 0.25 * scale 652 this_b_height = self._button_height * 0.82 * scale 653 self._gather_button = btn = ba.buttonwidget( 654 parent=self._root_widget, 655 position=(this_h - this_b_width * 0.5, v), 656 size=(this_b_width, this_b_height), 657 autoselect=self._use_autoselect, 658 button_type='square', 659 label='', 660 transition_delay=gather_delay, 661 on_activate_call=self._gather_press, 662 ) 663 ba.textwidget( 664 parent=self._root_widget, 665 position=(this_h, v + self._button_height * 0.33), 666 size=(0, 0), 667 scale=0.75, 668 transition_delay=gather_delay, 669 draw_controller=btn, 670 color=(0.75, 1.0, 0.7), 671 maxwidth=self._button_width * 0.33, 672 text=ba.Lstr(resource='gatherWindow.titleText'), 673 h_align='center', 674 v_align='center', 675 ) 676 icon_size = this_b_width * 0.6 677 ba.imagewidget( 678 parent=self._root_widget, 679 size=(icon_size, icon_size), 680 draw_controller=btn, 681 transition_delay=gather_delay, 682 position=(this_h - 0.5 * icon_size, v + 0.31 * this_b_height), 683 texture=ba.gettexture('usersButton'), 684 ) 685 686 # Play button. 687 h, v, scale = positions[self._p_index] 688 self._p_index += 1 689 self._start_button = start_button = ba.buttonwidget( 690 parent=self._root_widget, 691 position=(h - play_button_width * 0.5 * scale, v), 692 size=(play_button_width, play_button_height), 693 autoselect=self._use_autoselect, 694 scale=scale, 695 text_res_scale=2.0, 696 label=ba.Lstr(resource='playText'), 697 transition_delay=self._t_delay_play, 698 on_activate_call=self._play_press, 699 ) 700 ba.containerwidget( 701 edit=self._root_widget, 702 start_button=start_button, 703 selected_child=start_button, 704 ) 705 v = v + foof 706 watch_delay = ( 707 0.0 708 if self._t_delay_play == 0.0 709 else max(0.0, self._t_delay_play - 0.1) 710 ) 711 this_h = h + play_button_width * 0.5 * scale + 40 * scale 712 this_b_width = self._button_width * 0.25 * scale 713 this_b_height = self._button_height * 0.82 * scale 714 self._watch_button = btn = ba.buttonwidget( 715 parent=self._root_widget, 716 position=(this_h - this_b_width * 0.5, v), 717 size=(this_b_width, this_b_height), 718 autoselect=self._use_autoselect, 719 button_type='square', 720 label='', 721 transition_delay=watch_delay, 722 on_activate_call=self._watch_press, 723 ) 724 ba.textwidget( 725 parent=self._root_widget, 726 position=(this_h, v + self._button_height * 0.33), 727 size=(0, 0), 728 scale=0.75, 729 transition_delay=watch_delay, 730 color=(0.75, 1.0, 0.7), 731 draw_controller=btn, 732 maxwidth=self._button_width * 0.33, 733 text=ba.Lstr(resource='watchWindow.titleText'), 734 h_align='center', 735 v_align='center', 736 ) 737 icon_size = this_b_width * 0.55 738 ba.imagewidget( 739 parent=self._root_widget, 740 size=(icon_size, icon_size), 741 draw_controller=btn, 742 transition_delay=watch_delay, 743 position=(this_h - 0.5 * icon_size, v + 0.33 * this_b_height), 744 texture=ba.gettexture('tv'), 745 ) 746 if not self._in_game and enable_account_button: 747 this_b_width = self._button_width 748 h, v, scale = positions[self._p_index] 749 self._p_index += 1 750 self._account_button = ba.buttonwidget( 751 parent=self._root_widget, 752 position=(h - this_b_width * 0.5 * scale, v), 753 size=(this_b_width, self._button_height), 754 scale=scale, 755 label=account_type_name, 756 autoselect=self._use_autoselect, 757 on_activate_call=account_type_call, 758 textcolor=account_textcolor, 759 icon=account_type_icon, 760 icon_color=account_type_icon_color, 761 transition_delay=self._tdelay, 762 enable_sound=account_type_enable_button_sound, 763 ) 764 765 # Scattered eggs on easter. 766 if ( 767 ba.internal.get_v1_account_misc_read_val('easter', False) 768 and not self._in_game 769 ): 770 icon_size = 32 771 ba.imagewidget( 772 parent=self._root_widget, 773 position=( 774 h - icon_size * 0.5 + 35, 775 v 776 + self._button_height * scale 777 - icon_size * 0.24 778 + 1.5, 779 ), 780 transition_delay=self._tdelay, 781 size=(icon_size, icon_size), 782 texture=ba.gettexture('egg2'), 783 tilt_scale=0.0, 784 ) 785 self._tdelay += self._t_delay_inc 786 else: 787 self._account_button = None 788 789 # How-to-play button. 790 h, v, scale = positions[self._p_index] 791 self._p_index += 1 792 btn = ba.buttonwidget( 793 parent=self._root_widget, 794 position=(h - self._button_width * 0.5 * scale, v), 795 scale=scale, 796 autoselect=self._use_autoselect, 797 size=(self._button_width, self._button_height), 798 label=ba.Lstr(resource=self._r + '.howToPlayText'), 799 transition_delay=self._tdelay, 800 on_activate_call=self._howtoplay, 801 ) 802 self._how_to_play_button = btn 803 804 # Scattered eggs on easter. 805 if ( 806 ba.internal.get_v1_account_misc_read_val('easter', False) 807 and not self._in_game 808 ): 809 icon_size = 28 810 ba.imagewidget( 811 parent=self._root_widget, 812 position=( 813 h - icon_size * 0.5 + 30, 814 v + self._button_height * scale - icon_size * 0.24 + 1.5, 815 ), 816 transition_delay=self._tdelay, 817 size=(icon_size, icon_size), 818 texture=ba.gettexture('egg4'), 819 tilt_scale=0.0, 820 ) 821 # Credits button. 822 self._tdelay += self._t_delay_inc 823 h, v, scale = positions[self._p_index] 824 self._p_index += 1 825 self._credits_button = ba.buttonwidget( 826 parent=self._root_widget, 827 position=(h - self._button_width * 0.5 * scale, v), 828 size=(self._button_width, self._button_height), 829 autoselect=self._use_autoselect, 830 label=ba.Lstr(resource=self._r + '.creditsText'), 831 scale=scale, 832 transition_delay=self._tdelay, 833 on_activate_call=self._credits, 834 ) 835 self._tdelay += self._t_delay_inc 836 return h, v, scale 837 838 def _refresh_in_game( 839 self, positions: list[tuple[float, float, float]] 840 ) -> tuple[float, float, float]: 841 # pylint: disable=too-many-branches 842 # pylint: disable=too-many-locals 843 # pylint: disable=too-many-statements 844 custom_menu_entries: list[dict[str, Any]] = [] 845 session = ba.internal.get_foreground_host_session() 846 if session is not None: 847 try: 848 custom_menu_entries = session.get_custom_menu_entries() 849 for cme in custom_menu_entries: 850 if ( 851 not isinstance(cme, dict) 852 or 'label' not in cme 853 or not isinstance(cme['label'], (str, ba.Lstr)) 854 or 'call' not in cme 855 or not callable(cme['call']) 856 ): 857 raise ValueError( 858 'invalid custom menu entry: ' + str(cme) 859 ) 860 except Exception: 861 custom_menu_entries = [] 862 ba.print_exception( 863 f'Error getting custom menu entries for {session}' 864 ) 865 self._width = 250.0 866 self._height = 250.0 if self._input_player else 180.0 867 if (self._is_demo or self._is_arcade) and self._input_player: 868 self._height -= 40 869 if not self._have_settings_button: 870 self._height -= 50 871 if self._connected_to_remote_player: 872 # In this case we have a leave *and* a disconnect button. 873 self._height += 50 874 self._height += 50 * (len(custom_menu_entries)) 875 uiscale = ba.app.ui.uiscale 876 ba.containerwidget( 877 edit=self._root_widget, 878 size=(self._width, self._height), 879 scale=( 880 2.15 881 if uiscale is ba.UIScale.SMALL 882 else 1.6 883 if uiscale is ba.UIScale.MEDIUM 884 else 1.0 885 ), 886 ) 887 h = 125.0 888 v = self._height - 80.0 if self._input_player else self._height - 60 889 h_offset = 0 890 d_h_offset = 0 891 v_offset = -50 892 for _i in range(6 + len(custom_menu_entries)): 893 positions.append((h, v, 1.0)) 894 v += v_offset 895 h += h_offset 896 h_offset += d_h_offset 897 self._start_button = None 898 ba.app.pause() 899 900 # Player name if applicable. 901 if self._input_player: 902 player_name = self._input_player.getname() 903 h, v, scale = positions[self._p_index] 904 v += 35 905 ba.textwidget( 906 parent=self._root_widget, 907 position=(h - self._button_width / 2, v), 908 size=(self._button_width, self._button_height), 909 color=(1, 1, 1, 0.5), 910 scale=0.7, 911 h_align='center', 912 text=ba.Lstr(value=player_name), 913 ) 914 else: 915 player_name = '' 916 h, v, scale = positions[self._p_index] 917 self._p_index += 1 918 btn = ba.buttonwidget( 919 parent=self._root_widget, 920 position=(h - self._button_width / 2, v), 921 size=(self._button_width, self._button_height), 922 scale=scale, 923 label=ba.Lstr(resource=self._r + '.resumeText'), 924 autoselect=self._use_autoselect, 925 on_activate_call=self._resume, 926 ) 927 ba.containerwidget(edit=self._root_widget, cancel_button=btn) 928 929 # Add any custom options defined by the current game. 930 for entry in custom_menu_entries: 931 h, v, scale = positions[self._p_index] 932 self._p_index += 1 933 934 # Ask the entry whether we should resume when we call 935 # it (defaults to true). 936 resume = bool(entry.get('resume_on_call', True)) 937 938 if resume: 939 call = ba.Call(self._resume_and_call, entry['call']) 940 else: 941 call = ba.Call(entry['call'], ba.WeakCall(self._resume)) 942 943 ba.buttonwidget( 944 parent=self._root_widget, 945 position=(h - self._button_width / 2, v), 946 size=(self._button_width, self._button_height), 947 scale=scale, 948 on_activate_call=call, 949 label=entry['label'], 950 autoselect=self._use_autoselect, 951 ) 952 # Add a 'leave' button if the menu-owner has a player. 953 if (self._input_player or self._connected_to_remote_player) and not ( 954 self._is_demo or self._is_arcade 955 ): 956 h, v, scale = positions[self._p_index] 957 self._p_index += 1 958 btn = ba.buttonwidget( 959 parent=self._root_widget, 960 position=(h - self._button_width / 2, v), 961 size=(self._button_width, self._button_height), 962 scale=scale, 963 on_activate_call=self._leave, 964 label='', 965 autoselect=self._use_autoselect, 966 ) 967 968 if ( 969 player_name != '' 970 and player_name[0] != '<' 971 and player_name[-1] != '>' 972 ): 973 txt = ba.Lstr( 974 resource=self._r + '.justPlayerText', 975 subs=[('${NAME}', player_name)], 976 ) 977 else: 978 txt = ba.Lstr(value=player_name) 979 ba.textwidget( 980 parent=self._root_widget, 981 position=( 982 h, 983 v 984 + self._button_height 985 * (0.64 if player_name != '' else 0.5), 986 ), 987 size=(0, 0), 988 text=ba.Lstr(resource=self._r + '.leaveGameText'), 989 scale=(0.83 if player_name != '' else 1.0), 990 color=(0.75, 1.0, 0.7), 991 h_align='center', 992 v_align='center', 993 draw_controller=btn, 994 maxwidth=self._button_width * 0.9, 995 ) 996 ba.textwidget( 997 parent=self._root_widget, 998 position=(h, v + self._button_height * 0.27), 999 size=(0, 0), 1000 text=txt, 1001 color=(0.75, 1.0, 0.7), 1002 h_align='center', 1003 v_align='center', 1004 draw_controller=btn, 1005 scale=0.45, 1006 maxwidth=self._button_width * 0.9, 1007 ) 1008 return h, v, scale 1009 1010 def _change_replay_speed(self, offs: int) -> None: 1011 if not self._replay_speed_text: 1012 if ba.do_once(): 1013 print('_change_replay_speed called without widget') 1014 return 1015 ba.internal.set_replay_speed_exponent( 1016 ba.internal.get_replay_speed_exponent() + offs 1017 ) 1018 actual_speed = pow(2.0, ba.internal.get_replay_speed_exponent()) 1019 ba.textwidget( 1020 edit=self._replay_speed_text, 1021 text=ba.Lstr( 1022 resource='watchWindow.playbackSpeedText', 1023 subs=[('${SPEED}', str(actual_speed))], 1024 ), 1025 ) 1026 1027 def _quit(self) -> None: 1028 # pylint: disable=cyclic-import 1029 from bastd.ui.confirm import QuitWindow 1030 1031 QuitWindow(origin_widget=self._quit_button) 1032 1033 def _demo_menu_press(self) -> None: 1034 # pylint: disable=cyclic-import 1035 from bastd.ui.kiosk import KioskWindow 1036 1037 self._save_state() 1038 ba.containerwidget(edit=self._root_widget, transition='out_right') 1039 ba.app.ui.set_main_menu_window( 1040 KioskWindow(transition='in_left').get_root_widget() 1041 ) 1042 1043 def _show_account_window(self) -> None: 1044 # pylint: disable=cyclic-import 1045 from bastd.ui.account.settings import AccountSettingsWindow 1046 1047 self._save_state() 1048 ba.containerwidget(edit=self._root_widget, transition='out_left') 1049 ba.app.ui.set_main_menu_window( 1050 AccountSettingsWindow( 1051 origin_widget=self._account_button 1052 ).get_root_widget() 1053 ) 1054 1055 def _on_store_pressed(self) -> None: 1056 # pylint: disable=cyclic-import 1057 from bastd.ui.store.browser import StoreBrowserWindow 1058 from bastd.ui.account import show_sign_in_prompt 1059 1060 if ba.internal.get_v1_account_state() != 'signed_in': 1061 show_sign_in_prompt() 1062 return 1063 self._save_state() 1064 ba.containerwidget(edit=self._root_widget, transition='out_left') 1065 ba.app.ui.set_main_menu_window( 1066 StoreBrowserWindow( 1067 origin_widget=self._store_button 1068 ).get_root_widget() 1069 ) 1070 1071 def _is_benchmark(self) -> bool: 1072 session = ba.internal.get_foreground_host_session() 1073 return ( 1074 getattr(session, 'benchmark_type', None) == 'cpu' 1075 or ba.app.stress_test_reset_timer is not None 1076 ) 1077 1078 def _confirm_end_game(self) -> None: 1079 # pylint: disable=cyclic-import 1080 from bastd.ui.confirm import ConfirmWindow 1081 1082 # FIXME: Currently we crash calling this on client-sessions. 1083 1084 # Select cancel by default; this occasionally gets called by accident 1085 # in a fit of button mashing and this will help reduce damage. 1086 ConfirmWindow( 1087 ba.Lstr(resource=self._r + '.exitToMenuText'), 1088 self._end_game, 1089 cancel_is_selected=True, 1090 ) 1091 1092 def _confirm_end_test(self) -> None: 1093 # pylint: disable=cyclic-import 1094 from bastd.ui.confirm import ConfirmWindow 1095 1096 # Select cancel by default; this occasionally gets called by accident 1097 # in a fit of button mashing and this will help reduce damage. 1098 ConfirmWindow( 1099 ba.Lstr(resource=self._r + '.exitToMenuText'), 1100 self._end_game, 1101 cancel_is_selected=True, 1102 ) 1103 1104 def _confirm_end_replay(self) -> None: 1105 # pylint: disable=cyclic-import 1106 from bastd.ui.confirm import ConfirmWindow 1107 1108 # Select cancel by default; this occasionally gets called by accident 1109 # in a fit of button mashing and this will help reduce damage. 1110 ConfirmWindow( 1111 ba.Lstr(resource=self._r + '.exitToMenuText'), 1112 self._end_game, 1113 cancel_is_selected=True, 1114 ) 1115 1116 def _confirm_leave_party(self) -> None: 1117 # pylint: disable=cyclic-import 1118 from bastd.ui.confirm import ConfirmWindow 1119 1120 # Select cancel by default; this occasionally gets called by accident 1121 # in a fit of button mashing and this will help reduce damage. 1122 ConfirmWindow( 1123 ba.Lstr(resource=self._r + '.leavePartyConfirmText'), 1124 self._leave_party, 1125 cancel_is_selected=True, 1126 ) 1127 1128 def _leave_party(self) -> None: 1129 ba.internal.disconnect_from_host() 1130 1131 def _end_game(self) -> None: 1132 if not self._root_widget: 1133 return 1134 ba.containerwidget(edit=self._root_widget, transition='out_left') 1135 ba.app.return_to_main_menu_session_gracefully(reset_ui=False) 1136 1137 def _leave(self) -> None: 1138 if self._input_player: 1139 self._input_player.remove_from_game() 1140 elif self._connected_to_remote_player: 1141 if self._input_device: 1142 self._input_device.remove_remote_player_from_game() 1143 self._resume() 1144 1145 def _credits(self) -> None: 1146 # pylint: disable=cyclic-import 1147 from bastd.ui.creditslist import CreditsListWindow 1148 1149 self._save_state() 1150 ba.containerwidget(edit=self._root_widget, transition='out_left') 1151 ba.app.ui.set_main_menu_window( 1152 CreditsListWindow( 1153 origin_widget=self._credits_button 1154 ).get_root_widget() 1155 ) 1156 1157 def _howtoplay(self) -> None: 1158 # pylint: disable=cyclic-import 1159 from bastd.ui.helpui import HelpWindow 1160 1161 self._save_state() 1162 ba.containerwidget(edit=self._root_widget, transition='out_left') 1163 ba.app.ui.set_main_menu_window( 1164 HelpWindow( 1165 main_menu=True, origin_widget=self._how_to_play_button 1166 ).get_root_widget() 1167 ) 1168 1169 def _settings(self) -> None: 1170 # pylint: disable=cyclic-import 1171 from bastd.ui.settings.allsettings import AllSettingsWindow 1172 1173 self._save_state() 1174 ba.containerwidget(edit=self._root_widget, transition='out_left') 1175 ba.app.ui.set_main_menu_window( 1176 AllSettingsWindow( 1177 origin_widget=self._settings_button 1178 ).get_root_widget() 1179 ) 1180 1181 def _resume_and_call(self, call: Callable[[], Any]) -> None: 1182 self._resume() 1183 call() 1184 1185 def _do_game_service_press(self) -> None: 1186 self._save_state() 1187 ba.internal.show_online_score_ui() 1188 1189 def _save_state(self) -> None: 1190 1191 # Don't do this for the in-game menu. 1192 if self._in_game: 1193 return 1194 sel = self._root_widget.get_selected_child() 1195 if sel == self._start_button: 1196 ba.app.ui.main_menu_selection = 'Start' 1197 elif sel == self._gather_button: 1198 ba.app.ui.main_menu_selection = 'Gather' 1199 elif sel == self._watch_button: 1200 ba.app.ui.main_menu_selection = 'Watch' 1201 elif sel == self._how_to_play_button: 1202 ba.app.ui.main_menu_selection = 'HowToPlay' 1203 elif sel == self._credits_button: 1204 ba.app.ui.main_menu_selection = 'Credits' 1205 elif sel == self._settings_button: 1206 ba.app.ui.main_menu_selection = 'Settings' 1207 elif sel == self._account_button: 1208 ba.app.ui.main_menu_selection = 'Account' 1209 elif sel == self._store_button: 1210 ba.app.ui.main_menu_selection = 'Store' 1211 elif sel == self._quit_button: 1212 ba.app.ui.main_menu_selection = 'Quit' 1213 elif sel == self._demo_menu_button: 1214 ba.app.ui.main_menu_selection = 'DemoMenu' 1215 else: 1216 print('unknown widget in main menu store selection:', sel) 1217 ba.app.ui.main_menu_selection = 'Start' 1218 1219 def _restore_state(self) -> None: 1220 # pylint: disable=too-many-branches 1221 1222 # Don't do this for the in-game menu. 1223 if self._in_game: 1224 return 1225 sel_name = ba.app.ui.main_menu_selection 1226 sel: ba.Widget | None 1227 if sel_name is None: 1228 sel_name = 'Start' 1229 if sel_name == 'HowToPlay': 1230 sel = self._how_to_play_button 1231 elif sel_name == 'Gather': 1232 sel = self._gather_button 1233 elif sel_name == 'Watch': 1234 sel = self._watch_button 1235 elif sel_name == 'Credits': 1236 sel = self._credits_button 1237 elif sel_name == 'Settings': 1238 sel = self._settings_button 1239 elif sel_name == 'Account': 1240 sel = self._account_button 1241 elif sel_name == 'Store': 1242 sel = self._store_button 1243 elif sel_name == 'Quit': 1244 sel = self._quit_button 1245 elif sel_name == 'DemoMenu': 1246 sel = self._demo_menu_button 1247 else: 1248 sel = self._start_button 1249 if sel is not None: 1250 ba.containerwidget(edit=self._root_widget, selected_child=sel) 1251 1252 def _gather_press(self) -> None: 1253 # pylint: disable=cyclic-import 1254 from bastd.ui.gather import GatherWindow 1255 1256 self._save_state() 1257 ba.containerwidget(edit=self._root_widget, transition='out_left') 1258 ba.app.ui.set_main_menu_window( 1259 GatherWindow(origin_widget=self._gather_button).get_root_widget() 1260 ) 1261 1262 def _watch_press(self) -> None: 1263 # pylint: disable=cyclic-import 1264 from bastd.ui.watch import WatchWindow 1265 1266 self._save_state() 1267 ba.containerwidget(edit=self._root_widget, transition='out_left') 1268 ba.app.ui.set_main_menu_window( 1269 WatchWindow(origin_widget=self._watch_button).get_root_widget() 1270 ) 1271 1272 def _play_press(self) -> None: 1273 # pylint: disable=cyclic-import 1274 from bastd.ui.play import PlayWindow 1275 1276 self._save_state() 1277 ba.containerwidget(edit=self._root_widget, transition='out_left') 1278 1279 ba.app.ui.selecting_private_party_playlist = False 1280 ba.app.ui.set_main_menu_window( 1281 PlayWindow(origin_widget=self._start_button).get_root_widget() 1282 ) 1283 1284 def _resume(self) -> None: 1285 ba.app.resume() 1286 if self._root_widget: 1287 ba.containerwidget(edit=self._root_widget, transition='out_right') 1288 ba.app.ui.clear_main_menu_window() 1289 1290 # If there's callbacks waiting for this window to go away, call them. 1291 for call in ba.app.main_menu_resume_callbacks: 1292 call() 1293 del ba.app.main_menu_resume_callbacks[:]
The main menu window, both in-game and in the main menu session.
MainMenuWindow(transition: str | None = 'in_right')
21 def __init__(self, transition: str | None = 'in_right'): 22 # pylint: disable=cyclic-import 23 import threading 24 from bastd.mainmenu import MainMenuSession 25 26 self._in_game = not isinstance( 27 ba.internal.get_foreground_host_session(), MainMenuSession 28 ) 29 30 # Preload some modules we use in a background thread so we won't 31 # have a visual hitch when the user taps them. 32 threading.Thread(target=self._preload_modules).start() 33 34 if not self._in_game: 35 ba.set_analytics_screen('Main Menu') 36 self._show_remote_app_info_on_first_launch() 37 38 # Make a vanilla container; we'll modify it to our needs in refresh. 39 super().__init__( 40 root_widget=ba.containerwidget( 41 transition=transition, 42 toolbar_visibility='menu_minimal_no_back' 43 if self._in_game 44 else 'menu_minimal_no_back', 45 ) 46 ) 47 48 # Grab this stuff in case it changes. 49 self._is_demo = ba.app.demo_mode 50 self._is_arcade = ba.app.arcade_mode 51 self._is_iircade = ba.app.iircade_mode 52 53 self._tdelay = 0.0 54 self._t_delay_inc = 0.02 55 self._t_delay_play = 1.7 56 self._p_index = 0 57 self._use_autoselect = True 58 self._button_width = 200.0 59 self._button_height = 45.0 60 self._width = 100.0 61 self._height = 100.0 62 self._demo_menu_button: ba.Widget | None = None 63 self._gather_button: ba.Widget | None = None 64 self._start_button: ba.Widget | None = None 65 self._watch_button: ba.Widget | None = None 66 self._account_button: ba.Widget | None = None 67 self._how_to_play_button: ba.Widget | None = None 68 self._credits_button: ba.Widget | None = None 69 self._settings_button: ba.Widget | None = None 70 self._next_refresh_allow_time = 0.0 71 72 self._store_char_tex = self._get_store_char_tex() 73 74 self._refresh() 75 self._restore_state() 76 77 # Keep an eye on a few things and refresh if they change. 78 self._account_state = ba.internal.get_v1_account_state() 79 self._account_state_num = ba.internal.get_v1_account_state_num() 80 self._account_type = ( 81 ba.internal.get_v1_account_type() 82 if self._account_state == 'signed_in' 83 else None 84 ) 85 self._refresh_timer = ba.Timer( 86 0.27, 87 ba.WeakCall(self._check_refresh), 88 repeat=True, 89 timetype=ba.TimeType.REAL, 90 )
Inherited Members
- ba.ui.Window
- get_root_widget