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