bascenev1lib.mainmenu
Session and Activity for displaying the main menu bg.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Session and Activity for displaying the main menu bg.""" 4 5from __future__ import annotations 6 7import time 8import random 9import weakref 10from typing import TYPE_CHECKING, override 11 12import bascenev1 as bs 13import bauiv1 as bui 14 15if TYPE_CHECKING: 16 from typing import Any 17 18 19class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): 20 """Activity showing the rotating main menu bg stuff.""" 21 22 _stdassets = bs.Dependency(bs.AssetPackage, 'stdassets@1') 23 24 def __init__(self, settings: dict): 25 super().__init__(settings) 26 self._logo_node: bs.Node | None = None 27 self._custom_logo_tex_name: str | None = None 28 self._word_actors: list[bs.Actor] = [] 29 self.my_name: bs.NodeActor | None = None 30 self._host_is_navigating_text: bs.NodeActor | None = None 31 self.version: bs.NodeActor | None = None 32 self.beta_info: bs.NodeActor | None = None 33 self.beta_info_2: bs.NodeActor | None = None 34 self.bottom: bs.NodeActor | None = None 35 self.vr_bottom_fill: bs.NodeActor | None = None 36 self.vr_top_fill: bs.NodeActor | None = None 37 self.terrain: bs.NodeActor | None = None 38 self.trees: bs.NodeActor | None = None 39 self.bgterrain: bs.NodeActor | None = None 40 self._ts = 0.86 41 self._language: str | None = None 42 self._update_timer: bs.Timer | None = None 43 self._news: NewsDisplay | None = None 44 self._attract_mode_timer: bs.Timer | None = None 45 46 @override 47 def on_transition_in(self) -> None: 48 # pylint: disable=too-many-locals 49 # pylint: disable=too-many-statements 50 super().on_transition_in() 51 random.seed(123) 52 app = bs.app 53 env = app.env 54 assert app.classic is not None 55 56 plus = bs.app.plus 57 assert plus is not None 58 59 # Throw up some text that only clients can see so they know that the 60 # host is navigating menus while they're just staring at an 61 # empty-ish screen. 62 tval = bs.Lstr( 63 resource='hostIsNavigatingMenusText', 64 subs=[('${HOST}', plus.get_v1_account_display_string())], 65 ) 66 self._host_is_navigating_text = bs.NodeActor( 67 bs.newnode( 68 'text', 69 attrs={ 70 'text': tval, 71 'client_only': True, 72 'position': (0, -200), 73 'flatness': 1.0, 74 'h_align': 'center', 75 }, 76 ) 77 ) 78 if ( 79 not app.classic.main_menu_did_initial_transition 80 and self.my_name is not None 81 ): 82 assert self.my_name.node 83 bs.animate(self.my_name.node, 'opacity', {2.3: 0, 3.0: 1.0}) 84 85 # Throw in test build info. 86 self.beta_info = self.beta_info_2 = None 87 if env.test: 88 pos = (230, -5) 89 self.beta_info = bs.NodeActor( 90 bs.newnode( 91 'text', 92 attrs={ 93 'v_attach': 'center', 94 'h_align': 'center', 95 'color': (1, 1, 1, 1), 96 'shadow': 0.5, 97 'flatness': 0.5, 98 'scale': 1, 99 'vr_depth': -60, 100 'position': pos, 101 'text': bs.Lstr(resource='testBuildText'), 102 }, 103 ) 104 ) 105 if not app.classic.main_menu_did_initial_transition: 106 assert self.beta_info.node 107 bs.animate(self.beta_info.node, 'opacity', {1.3: 0, 1.8: 1.0}) 108 109 mesh = bs.getmesh('thePadLevel') 110 trees_mesh = bs.getmesh('trees') 111 bottom_mesh = bs.getmesh('thePadLevelBottom') 112 color_texture = bs.gettexture('thePadLevelColor') 113 trees_texture = bs.gettexture('treesColor') 114 bgtex = bs.gettexture('menuBG') 115 bgmesh = bs.getmesh('thePadBG') 116 117 # Load these last since most platforms don't use them. 118 vr_bottom_fill_mesh = bs.getmesh('thePadVRFillBottom') 119 vr_top_fill_mesh = bs.getmesh('thePadVRFillTop') 120 121 gnode = self.globalsnode 122 gnode.camera_mode = 'rotate' 123 124 tint = (1.14, 1.1, 1.0) 125 gnode.tint = tint 126 gnode.ambient_color = (1.06, 1.04, 1.03) 127 gnode.vignette_outer = (0.45, 0.55, 0.54) 128 gnode.vignette_inner = (0.99, 0.98, 0.98) 129 130 self.bottom = bs.NodeActor( 131 bs.newnode( 132 'terrain', 133 attrs={ 134 'mesh': bottom_mesh, 135 'lighting': False, 136 'reflection': 'soft', 137 'reflection_scale': [0.45], 138 'color_texture': color_texture, 139 }, 140 ) 141 ) 142 self.vr_bottom_fill = bs.NodeActor( 143 bs.newnode( 144 'terrain', 145 attrs={ 146 'mesh': vr_bottom_fill_mesh, 147 'lighting': False, 148 'vr_only': True, 149 'color_texture': color_texture, 150 }, 151 ) 152 ) 153 self.vr_top_fill = bs.NodeActor( 154 bs.newnode( 155 'terrain', 156 attrs={ 157 'mesh': vr_top_fill_mesh, 158 'vr_only': True, 159 'lighting': False, 160 'color_texture': bgtex, 161 }, 162 ) 163 ) 164 self.terrain = bs.NodeActor( 165 bs.newnode( 166 'terrain', 167 attrs={ 168 'mesh': mesh, 169 'color_texture': color_texture, 170 'reflection': 'soft', 171 'reflection_scale': [0.3], 172 }, 173 ) 174 ) 175 self.trees = bs.NodeActor( 176 bs.newnode( 177 'terrain', 178 attrs={ 179 'mesh': trees_mesh, 180 'lighting': False, 181 'reflection': 'char', 182 'reflection_scale': [0.1], 183 'color_texture': trees_texture, 184 }, 185 ) 186 ) 187 self.bgterrain = bs.NodeActor( 188 bs.newnode( 189 'terrain', 190 attrs={ 191 'mesh': bgmesh, 192 'color': (0.92, 0.91, 0.9), 193 'lighting': False, 194 'background': True, 195 'color_texture': bgtex, 196 }, 197 ) 198 ) 199 200 self._update_timer = bs.Timer(1.0, self._update, repeat=True) 201 self._update() 202 203 # Hopefully this won't hitch but lets space these out anyway. 204 bs.add_clean_frame_callback(bs.WeakCall(self._start_preloads)) 205 206 random.seed() 207 208 # Need to update this for toolbar mode; currenly doesn't fit. 209 if bool(False): 210 if not (env.demo or env.arcade): 211 self._news = NewsDisplay(self) 212 213 self._attract_mode_timer = bs.Timer( 214 3.12, self._update_attract_mode, repeat=True 215 ) 216 217 app.classic.invoke_main_menu_ui() 218 219 app.classic.main_menu_did_initial_transition = True 220 221 def _update(self) -> None: 222 # pylint: disable=too-many-locals 223 # pylint: disable=too-many-statements 224 app = bs.app 225 env = app.env 226 assert app.classic is not None 227 228 # Update logo in case it changes. 229 if self._logo_node: 230 custom_texture = self._get_custom_logo_tex_name() 231 if custom_texture != self._custom_logo_tex_name: 232 self._custom_logo_tex_name = custom_texture 233 self._logo_node.texture = bs.gettexture( 234 custom_texture if custom_texture is not None else 'logo' 235 ) 236 self._logo_node.mesh_opaque = ( 237 None if custom_texture is not None else bs.getmesh('logo') 238 ) 239 self._logo_node.mesh_transparent = ( 240 None 241 if custom_texture is not None 242 else bs.getmesh('logoTransparent') 243 ) 244 245 # If language has changed, recreate our logo text/graphics. 246 lang = app.lang.language 247 if lang != self._language: 248 self._language = lang 249 y = -15 250 base_scale = 1.1 251 self._word_actors = [] 252 base_delay = 1.0 253 delay = base_delay 254 delay_inc = 0.02 255 256 # Come on faster after the first time. 257 if app.classic.main_menu_did_initial_transition: 258 base_delay = 0.0 259 delay = base_delay 260 delay_inc = 0.02 261 262 # We draw higher in kiosk mode (make sure to test this 263 # when making adjustments) for now we're hard-coded for 264 # a few languages.. should maybe look into generalizing this?.. 265 if app.lang.language == 'Chinese': 266 base_x = -270.0 267 x = base_x - 20.0 268 spacing = 85.0 * base_scale 269 y_extra = 0.0 if (env.demo or env.arcade) else 0.0 270 self._make_logo( 271 x - 110 + 50, 272 113 + y + 1.2 * y_extra, 273 0.34 * base_scale, 274 delay=base_delay + 0.1, 275 custom_texture='chTitleChar1', 276 jitter_scale=2.0, 277 vr_depth_offset=-30, 278 ) 279 x += spacing 280 delay += delay_inc 281 self._make_logo( 282 x - 10 + 50, 283 110 + y + 1.2 * y_extra, 284 0.31 * base_scale, 285 delay=base_delay + 0.15, 286 custom_texture='chTitleChar2', 287 jitter_scale=2.0, 288 vr_depth_offset=-30, 289 ) 290 x += 2.0 * spacing 291 delay += delay_inc 292 self._make_logo( 293 x + 180 - 140, 294 110 + y + 1.2 * y_extra, 295 0.3 * base_scale, 296 delay=base_delay + 0.25, 297 custom_texture='chTitleChar3', 298 jitter_scale=2.0, 299 vr_depth_offset=-30, 300 ) 301 x += spacing 302 delay += delay_inc 303 self._make_logo( 304 x + 241 - 120, 305 110 + y + 1.2 * y_extra, 306 0.31 * base_scale, 307 delay=base_delay + 0.3, 308 custom_texture='chTitleChar4', 309 jitter_scale=2.0, 310 vr_depth_offset=-30, 311 ) 312 x += spacing 313 delay += delay_inc 314 self._make_logo( 315 x + 300 - 90, 316 105 + y + 1.2 * y_extra, 317 0.34 * base_scale, 318 delay=base_delay + 0.35, 319 custom_texture='chTitleChar5', 320 jitter_scale=2.0, 321 vr_depth_offset=-30, 322 ) 323 self._make_logo( 324 base_x + 155, 325 146 + y + 1.2 * y_extra, 326 0.28 * base_scale, 327 delay=base_delay + 0.2, 328 rotate=-7, 329 ) 330 else: 331 base_x = -170 332 x = base_x - 20 333 spacing = 55 * base_scale 334 y_extra = 0 if (env.demo or env.arcade) else 0 335 xv1 = x 336 delay1 = delay 337 for shadow in (True, False): 338 x = xv1 339 delay = delay1 340 self._make_word( 341 'B', 342 x - 50, 343 y - 14 + 0.8 * y_extra, 344 scale=1.3 * base_scale, 345 delay=delay, 346 vr_depth_offset=3, 347 shadow=shadow, 348 ) 349 x += spacing 350 delay += delay_inc 351 self._make_word( 352 'm', 353 x, 354 y + y_extra, 355 delay=delay, 356 scale=base_scale, 357 shadow=shadow, 358 ) 359 x += spacing * 1.25 360 delay += delay_inc 361 self._make_word( 362 'b', 363 x, 364 y + y_extra - 10, 365 delay=delay, 366 scale=1.1 * base_scale, 367 vr_depth_offset=5, 368 shadow=shadow, 369 ) 370 x += spacing * 0.85 371 delay += delay_inc 372 self._make_word( 373 'S', 374 x, 375 y - 15 + 0.8 * y_extra, 376 scale=1.35 * base_scale, 377 delay=delay, 378 vr_depth_offset=14, 379 shadow=shadow, 380 ) 381 x += spacing 382 delay += delay_inc 383 self._make_word( 384 'q', 385 x, 386 y + y_extra, 387 delay=delay, 388 scale=base_scale, 389 shadow=shadow, 390 ) 391 x += spacing * 0.9 392 delay += delay_inc 393 self._make_word( 394 'u', 395 x, 396 y + y_extra, 397 delay=delay, 398 scale=base_scale, 399 vr_depth_offset=7, 400 shadow=shadow, 401 ) 402 x += spacing * 0.9 403 delay += delay_inc 404 self._make_word( 405 'a', 406 x, 407 y + y_extra, 408 delay=delay, 409 scale=base_scale, 410 shadow=shadow, 411 ) 412 x += spacing * 0.64 413 delay += delay_inc 414 self._make_word( 415 'd', 416 x, 417 y + y_extra - 10, 418 delay=delay, 419 scale=1.1 * base_scale, 420 vr_depth_offset=6, 421 shadow=shadow, 422 ) 423 self._make_logo( 424 base_x - 28, 425 125 + y + 1.2 * y_extra, 426 0.32 * base_scale, 427 delay=base_delay, 428 ) 429 430 def _make_word( 431 self, 432 word: str, 433 x: float, 434 y: float, 435 scale: float = 1.0, 436 delay: float = 0.0, 437 vr_depth_offset: float = 0.0, 438 shadow: bool = False, 439 ) -> None: 440 # pylint: disable=too-many-branches 441 # pylint: disable=too-many-locals 442 # pylint: disable=too-many-statements 443 if shadow: 444 word_obj = bs.NodeActor( 445 bs.newnode( 446 'text', 447 attrs={ 448 'position': (x, y), 449 'big': True, 450 'color': (0.0, 0.0, 0.2, 0.08), 451 'tilt_translate': 0.09, 452 'opacity_scales_shadow': False, 453 'shadow': 0.2, 454 'vr_depth': -130, 455 'v_align': 'center', 456 'project_scale': 0.97 * scale, 457 'scale': 1.0, 458 'text': word, 459 }, 460 ) 461 ) 462 self._word_actors.append(word_obj) 463 else: 464 word_obj = bs.NodeActor( 465 bs.newnode( 466 'text', 467 attrs={ 468 'position': (x, y), 469 'big': True, 470 'color': (1.2, 1.15, 1.15, 1.0), 471 'tilt_translate': 0.11, 472 'shadow': 0.2, 473 'vr_depth': -40 + vr_depth_offset, 474 'v_align': 'center', 475 'project_scale': scale, 476 'scale': 1.0, 477 'text': word, 478 }, 479 ) 480 ) 481 self._word_actors.append(word_obj) 482 483 # Add a bit of stop-motion-y jitter to the logo (unless we're in 484 # VR mode in which case its best to leave things still). 485 if not bs.app.env.vr: 486 cmb: bs.Node | None 487 cmb2: bs.Node | None 488 if not shadow: 489 cmb = bs.newnode( 490 'combine', owner=word_obj.node, attrs={'size': 2} 491 ) 492 else: 493 cmb = None 494 if shadow: 495 cmb2 = bs.newnode( 496 'combine', owner=word_obj.node, attrs={'size': 2} 497 ) 498 else: 499 cmb2 = None 500 if not shadow: 501 assert cmb and word_obj.node 502 cmb.connectattr('output', word_obj.node, 'position') 503 if shadow: 504 assert cmb2 and word_obj.node 505 cmb2.connectattr('output', word_obj.node, 'position') 506 keys = {} 507 keys2 = {} 508 time_v = 0.0 509 for _i in range(10): 510 val = x + (random.random() - 0.5) * 0.8 511 val2 = x + (random.random() - 0.5) * 0.8 512 keys[time_v * self._ts] = val 513 keys2[time_v * self._ts] = val2 + 5 514 time_v += random.random() * 0.1 515 if cmb is not None: 516 bs.animate(cmb, 'input0', keys, loop=True) 517 if cmb2 is not None: 518 bs.animate(cmb2, 'input0', keys2, loop=True) 519 keys = {} 520 keys2 = {} 521 time_v = 0 522 for _i in range(10): 523 val = y + (random.random() - 0.5) * 0.8 524 val2 = y + (random.random() - 0.5) * 0.8 525 keys[time_v * self._ts] = val 526 keys2[time_v * self._ts] = val2 - 9 527 time_v += random.random() * 0.1 528 if cmb is not None: 529 bs.animate(cmb, 'input1', keys, loop=True) 530 if cmb2 is not None: 531 bs.animate(cmb2, 'input1', keys2, loop=True) 532 533 if not shadow: 534 assert word_obj.node 535 bs.animate( 536 word_obj.node, 537 'project_scale', 538 {delay: 0.0, delay + 0.1: scale * 1.1, delay + 0.2: scale}, 539 ) 540 else: 541 assert word_obj.node 542 bs.animate( 543 word_obj.node, 544 'project_scale', 545 {delay: 0.0, delay + 0.1: scale * 1.1, delay + 0.2: scale}, 546 ) 547 548 def _get_custom_logo_tex_name(self) -> str | None: 549 plus = bui.app.plus 550 assert plus is not None 551 552 if plus.get_v1_account_misc_read_val('easter', False): 553 return 'logoEaster' 554 return None 555 556 # Pop the logo and menu in. 557 def _make_logo( 558 self, 559 x: float, 560 y: float, 561 scale: float, 562 delay: float, 563 custom_texture: str | None = None, 564 jitter_scale: float = 1.0, 565 rotate: float = 0.0, 566 vr_depth_offset: float = 0.0, 567 ) -> None: 568 # pylint: disable=too-many-locals 569 570 # Temp easter goodness. 571 if custom_texture is None: 572 custom_texture = self._get_custom_logo_tex_name() 573 self._custom_logo_tex_name = custom_texture 574 ltex = bs.gettexture( 575 custom_texture if custom_texture is not None else 'logo' 576 ) 577 mopaque = None if custom_texture is not None else bs.getmesh('logo') 578 mtrans = ( 579 None 580 if custom_texture is not None 581 else bs.getmesh('logoTransparent') 582 ) 583 logo = bs.NodeActor( 584 bs.newnode( 585 'image', 586 attrs={ 587 'texture': ltex, 588 'mesh_opaque': mopaque, 589 'mesh_transparent': mtrans, 590 'vr_depth': -10 + vr_depth_offset, 591 'rotate': rotate, 592 'attach': 'center', 593 'tilt_translate': 0.21, 594 'absolute_scale': True, 595 }, 596 ) 597 ) 598 self._logo_node = logo.node 599 self._word_actors.append(logo) 600 601 # Add a bit of stop-motion-y jitter to the logo (unless we're in 602 # VR mode in which case its best to leave things still). 603 assert logo.node 604 if not bs.app.env.vr: 605 cmb = bs.newnode('combine', owner=logo.node, attrs={'size': 2}) 606 cmb.connectattr('output', logo.node, 'position') 607 keys = {} 608 time_v = 0.0 609 610 # Gen some random keys for that stop-motion-y look 611 for _i in range(10): 612 keys[time_v] = x + (random.random() - 0.5) * 0.7 * jitter_scale 613 time_v += random.random() * 0.1 614 bs.animate(cmb, 'input0', keys, loop=True) 615 keys = {} 616 time_v = 0.0 617 for _i in range(10): 618 keys[time_v * self._ts] = ( 619 y + (random.random() - 0.5) * 0.7 * jitter_scale 620 ) 621 time_v += random.random() * 0.1 622 bs.animate(cmb, 'input1', keys, loop=True) 623 else: 624 logo.node.position = (x, y) 625 626 cmb = bs.newnode('combine', owner=logo.node, attrs={'size': 2}) 627 628 keys = { 629 delay: 0.0, 630 delay + 0.1: 700.0 * scale, 631 delay + 0.2: 600.0 * scale, 632 } 633 bs.animate(cmb, 'input0', keys) 634 bs.animate(cmb, 'input1', keys) 635 cmb.connectattr('output', logo.node, 'scale') 636 637 def _start_preloads(self) -> None: 638 # FIXME: The func that calls us back doesn't save/restore state 639 # or check for a dead activity so we have to do that ourself. 640 if self.expired: 641 return 642 with self.context: 643 _preload1() 644 645 def _start_menu_music() -> None: 646 assert bs.app.classic is not None 647 bs.setmusic(bs.MusicType.MENU) 648 649 bui.apptimer(0.5, _start_menu_music) 650 651 def _update_attract_mode(self) -> None: 652 if bui.app.classic is None: 653 return 654 655 if not bui.app.config.resolve('Show Demos When Idle'): 656 return 657 658 threshold = 20.0 659 660 # If we're idle *and* have been in this activity for that long, 661 # flip over to our cpu demo. 662 if bui.get_input_idle_time() > threshold and bs.time() > threshold: 663 bui.app.classic.run_stress_test( 664 playlist_type='Random', 665 playlist_name='__default__', 666 player_count=8, 667 round_duration=20, 668 attract_mode=True, 669 ) 670 671 672class NewsDisplay: 673 """Wrangles news display.""" 674 675 def __init__(self, activity: bs.Activity): 676 self._valid = True 677 self._message_duration = 10.0 678 self._message_spacing = 2.0 679 self._text: bs.NodeActor | None = None 680 self._activity = weakref.ref(activity) 681 self._phrases: list[str] = [] 682 self._used_phrases: list[str] = [] 683 self._phrase_change_timer: bs.Timer | None = None 684 685 # If we're signed in, fetch news immediately. Otherwise wait 686 # until we are signed in. 687 self._fetch_timer: bs.Timer | None = bs.Timer( 688 1.0, bs.WeakCall(self._try_fetching_news), repeat=True 689 ) 690 self._try_fetching_news() 691 692 # We now want to wait until we're signed in before fetching news. 693 def _try_fetching_news(self) -> None: 694 plus = bui.app.plus 695 assert plus is not None 696 697 if plus.get_v1_account_state() == 'signed_in': 698 self._fetch_news() 699 self._fetch_timer = None 700 701 def _fetch_news(self) -> None: 702 plus = bui.app.plus 703 assert plus is not None 704 705 assert bs.app.classic is not None 706 bs.app.classic.main_menu_last_news_fetch_time = time.time() 707 708 # UPDATE - We now just pull news from MRVs. 709 news = plus.get_v1_account_misc_read_val('n', None) 710 if news is not None: 711 self._got_news(news) 712 713 def _change_phrase(self) -> None: 714 from bascenev1lib.actor.text import Text 715 716 app = bs.app 717 assert app.classic is not None 718 719 # If our news is way out of date, lets re-request it; otherwise, 720 # rotate our phrase. 721 assert app.classic.main_menu_last_news_fetch_time is not None 722 if time.time() - app.classic.main_menu_last_news_fetch_time > 600.0: 723 self._fetch_news() 724 self._text = None 725 else: 726 if self._text is not None: 727 if not self._phrases: 728 for phr in self._used_phrases: 729 self._phrases.insert(0, phr) 730 val = self._phrases.pop() 731 if val == '__ACH__': 732 vrmode = app.env.vr 733 Text( 734 bs.Lstr(resource='nextAchievementsText'), 735 color=((1, 1, 1, 1) if vrmode else (0.95, 0.9, 1, 0.4)), 736 host_only=True, 737 maxwidth=200, 738 position=(-300, -35), 739 h_align=Text.HAlign.RIGHT, 740 transition=Text.Transition.FADE_IN, 741 scale=0.9 if vrmode else 0.7, 742 flatness=1.0 if vrmode else 0.6, 743 shadow=1.0 if vrmode else 0.5, 744 h_attach=Text.HAttach.CENTER, 745 v_attach=Text.VAttach.TOP, 746 transition_delay=1.0, 747 transition_out_delay=self._message_duration, 748 ).autoretain() 749 achs = [ 750 a 751 for a in app.classic.ach.achievements 752 if not a.complete 753 ] 754 if achs: 755 ach = achs.pop(random.randrange(min(4, len(achs)))) 756 ach.create_display( 757 -180, 758 -35, 759 1.0, 760 outdelay=self._message_duration, 761 style='news', 762 ) 763 if achs: 764 ach = achs.pop(random.randrange(min(8, len(achs)))) 765 ach.create_display( 766 180, 767 -35, 768 1.25, 769 outdelay=self._message_duration, 770 style='news', 771 ) 772 else: 773 spc = self._message_spacing 774 keys = { 775 spc: 0.0, 776 spc + 1.0: 1.0, 777 spc + self._message_duration - 1.0: 1.0, 778 spc + self._message_duration: 0.0, 779 } 780 assert self._text.node 781 bs.animate(self._text.node, 'opacity', keys) 782 # {k: v 783 # for k, v in list(keys.items())}) 784 self._text.node.text = val 785 786 def _got_news(self, news: str) -> None: 787 # Run this stuff in the context of our activity since we need to 788 # make nodes and stuff.. should fix the serverget call so it. 789 activity = self._activity() 790 if activity is None or activity.expired: 791 return 792 with activity.context: 793 self._phrases.clear() 794 795 # Show upcoming achievements in non-vr versions (currently 796 # too hard to read in vr). 797 self._used_phrases = (['__ACH__'] if not bs.app.env.vr else []) + [ 798 s for s in news.split('<br>\n') if s != '' 799 ] 800 self._phrase_change_timer = bs.Timer( 801 (self._message_duration + self._message_spacing), 802 bs.WeakCall(self._change_phrase), 803 repeat=True, 804 ) 805 806 assert bs.app.classic is not None 807 scl = ( 808 1.2 809 if (bs.app.ui_v1.uiscale is bs.UIScale.SMALL or bs.app.env.vr) 810 else 0.8 811 ) 812 813 color2 = (1, 1, 1, 1) if bs.app.env.vr else (0.7, 0.65, 0.75, 1.0) 814 shadow = 1.0 if bs.app.env.vr else 0.4 815 self._text = bs.NodeActor( 816 bs.newnode( 817 'text', 818 attrs={ 819 'v_attach': 'top', 820 'h_attach': 'center', 821 'h_align': 'center', 822 'vr_depth': -20, 823 'shadow': shadow, 824 'flatness': 0.8, 825 'v_align': 'top', 826 'color': color2, 827 'scale': scl, 828 'maxwidth': 900.0 / scl, 829 'position': (0, -10), 830 }, 831 ) 832 ) 833 self._change_phrase() 834 835 836def _preload1() -> None: 837 """Pre-load some assets a second or two into the main menu. 838 839 Helps avoid hitches later on. 840 """ 841 for mname in [ 842 'plasticEyesTransparent', 843 'playerLineup1Transparent', 844 'playerLineup2Transparent', 845 'playerLineup3Transparent', 846 'playerLineup4Transparent', 847 'angryComputerTransparent', 848 'scrollWidgetShort', 849 'windowBGBlotch', 850 ]: 851 bs.getmesh(mname) 852 for tname in ['playerLineup', 'lock']: 853 bs.gettexture(tname) 854 for tex in [ 855 'iconRunaround', 856 'iconOnslaught', 857 'medalComplete', 858 'medalBronze', 859 'medalSilver', 860 'medalGold', 861 'characterIconMask', 862 ]: 863 bs.gettexture(tex) 864 bs.gettexture('bg') 865 from bascenev1lib.actor.powerupbox import PowerupBoxFactory 866 867 PowerupBoxFactory.get() 868 bui.apptimer(0.1, _preload2) 869 870 871def _preload2() -> None: 872 # FIXME: Could integrate these loads with the classes that use them 873 # so they don't have to redundantly call the load 874 # (even if the actual result is cached). 875 for mname in ['powerup', 'powerupSimple']: 876 bs.getmesh(mname) 877 for tname in [ 878 'powerupBomb', 879 'powerupSpeed', 880 'powerupPunch', 881 'powerupIceBombs', 882 'powerupStickyBombs', 883 'powerupShield', 884 'powerupImpactBombs', 885 'powerupHealth', 886 ]: 887 bs.gettexture(tname) 888 for sname in [ 889 'powerup01', 890 'boxDrop', 891 'boxingBell', 892 'scoreHit01', 893 'scoreHit02', 894 'dripity', 895 'spawn', 896 'gong', 897 ]: 898 bs.getsound(sname) 899 from bascenev1lib.actor.bomb import BombFactory 900 901 BombFactory.get() 902 bui.apptimer(0.1, _preload3) 903 904 905def _preload3() -> None: 906 from bascenev1lib.actor.spazfactory import SpazFactory 907 908 for mname in ['bomb', 'bombSticky', 'impactBomb']: 909 bs.getmesh(mname) 910 for tname in [ 911 'bombColor', 912 'bombColorIce', 913 'bombStickyColor', 914 'impactBombColor', 915 'impactBombColorLit', 916 ]: 917 bs.gettexture(tname) 918 for sname in ['freeze', 'fuse01', 'activateBeep', 'warnBeep']: 919 bs.getsound(sname) 920 SpazFactory.get() 921 bui.apptimer(0.2, _preload4) 922 923 924def _preload4() -> None: 925 for tname in ['bar', 'meter', 'null', 'flagColor', 'achievementOutline']: 926 bs.gettexture(tname) 927 for mname in ['frameInset', 'meterTransparent', 'achievementOutline']: 928 bs.getmesh(mname) 929 for sname in ['metalHit', 'metalSkid', 'refWhistle', 'achievement']: 930 bs.getsound(sname) 931 from bascenev1lib.actor.flag import FlagFactory 932 933 FlagFactory.get() 934 935 936class MainMenuSession(bs.Session): 937 """Session that runs the main menu environment.""" 938 939 def __init__(self) -> None: 940 # Gather dependencies we'll need (just our activity). 941 self._activity_deps = bs.DependencySet(bs.Dependency(MainMenuActivity)) 942 943 super().__init__([self._activity_deps]) 944 self._locked = False 945 self.setactivity(bs.newactivity(MainMenuActivity)) 946 947 @override 948 def on_activity_end(self, activity: bs.Activity, results: Any) -> None: 949 if self._locked: 950 bui.unlock_all_input() 951 952 # Any ending activity leads us into the main menu one. 953 self.setactivity(bs.newactivity(MainMenuActivity)) 954 955 @override 956 def on_player_request(self, player: bs.SessionPlayer) -> bool: 957 # Reject all player requests. 958 return False
20class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): 21 """Activity showing the rotating main menu bg stuff.""" 22 23 _stdassets = bs.Dependency(bs.AssetPackage, 'stdassets@1') 24 25 def __init__(self, settings: dict): 26 super().__init__(settings) 27 self._logo_node: bs.Node | None = None 28 self._custom_logo_tex_name: str | None = None 29 self._word_actors: list[bs.Actor] = [] 30 self.my_name: bs.NodeActor | None = None 31 self._host_is_navigating_text: bs.NodeActor | None = None 32 self.version: bs.NodeActor | None = None 33 self.beta_info: bs.NodeActor | None = None 34 self.beta_info_2: bs.NodeActor | None = None 35 self.bottom: bs.NodeActor | None = None 36 self.vr_bottom_fill: bs.NodeActor | None = None 37 self.vr_top_fill: bs.NodeActor | None = None 38 self.terrain: bs.NodeActor | None = None 39 self.trees: bs.NodeActor | None = None 40 self.bgterrain: bs.NodeActor | None = None 41 self._ts = 0.86 42 self._language: str | None = None 43 self._update_timer: bs.Timer | None = None 44 self._news: NewsDisplay | None = None 45 self._attract_mode_timer: bs.Timer | None = None 46 47 @override 48 def on_transition_in(self) -> None: 49 # pylint: disable=too-many-locals 50 # pylint: disable=too-many-statements 51 super().on_transition_in() 52 random.seed(123) 53 app = bs.app 54 env = app.env 55 assert app.classic is not None 56 57 plus = bs.app.plus 58 assert plus is not None 59 60 # Throw up some text that only clients can see so they know that the 61 # host is navigating menus while they're just staring at an 62 # empty-ish screen. 63 tval = bs.Lstr( 64 resource='hostIsNavigatingMenusText', 65 subs=[('${HOST}', plus.get_v1_account_display_string())], 66 ) 67 self._host_is_navigating_text = bs.NodeActor( 68 bs.newnode( 69 'text', 70 attrs={ 71 'text': tval, 72 'client_only': True, 73 'position': (0, -200), 74 'flatness': 1.0, 75 'h_align': 'center', 76 }, 77 ) 78 ) 79 if ( 80 not app.classic.main_menu_did_initial_transition 81 and self.my_name is not None 82 ): 83 assert self.my_name.node 84 bs.animate(self.my_name.node, 'opacity', {2.3: 0, 3.0: 1.0}) 85 86 # Throw in test build info. 87 self.beta_info = self.beta_info_2 = None 88 if env.test: 89 pos = (230, -5) 90 self.beta_info = bs.NodeActor( 91 bs.newnode( 92 'text', 93 attrs={ 94 'v_attach': 'center', 95 'h_align': 'center', 96 'color': (1, 1, 1, 1), 97 'shadow': 0.5, 98 'flatness': 0.5, 99 'scale': 1, 100 'vr_depth': -60, 101 'position': pos, 102 'text': bs.Lstr(resource='testBuildText'), 103 }, 104 ) 105 ) 106 if not app.classic.main_menu_did_initial_transition: 107 assert self.beta_info.node 108 bs.animate(self.beta_info.node, 'opacity', {1.3: 0, 1.8: 1.0}) 109 110 mesh = bs.getmesh('thePadLevel') 111 trees_mesh = bs.getmesh('trees') 112 bottom_mesh = bs.getmesh('thePadLevelBottom') 113 color_texture = bs.gettexture('thePadLevelColor') 114 trees_texture = bs.gettexture('treesColor') 115 bgtex = bs.gettexture('menuBG') 116 bgmesh = bs.getmesh('thePadBG') 117 118 # Load these last since most platforms don't use them. 119 vr_bottom_fill_mesh = bs.getmesh('thePadVRFillBottom') 120 vr_top_fill_mesh = bs.getmesh('thePadVRFillTop') 121 122 gnode = self.globalsnode 123 gnode.camera_mode = 'rotate' 124 125 tint = (1.14, 1.1, 1.0) 126 gnode.tint = tint 127 gnode.ambient_color = (1.06, 1.04, 1.03) 128 gnode.vignette_outer = (0.45, 0.55, 0.54) 129 gnode.vignette_inner = (0.99, 0.98, 0.98) 130 131 self.bottom = bs.NodeActor( 132 bs.newnode( 133 'terrain', 134 attrs={ 135 'mesh': bottom_mesh, 136 'lighting': False, 137 'reflection': 'soft', 138 'reflection_scale': [0.45], 139 'color_texture': color_texture, 140 }, 141 ) 142 ) 143 self.vr_bottom_fill = bs.NodeActor( 144 bs.newnode( 145 'terrain', 146 attrs={ 147 'mesh': vr_bottom_fill_mesh, 148 'lighting': False, 149 'vr_only': True, 150 'color_texture': color_texture, 151 }, 152 ) 153 ) 154 self.vr_top_fill = bs.NodeActor( 155 bs.newnode( 156 'terrain', 157 attrs={ 158 'mesh': vr_top_fill_mesh, 159 'vr_only': True, 160 'lighting': False, 161 'color_texture': bgtex, 162 }, 163 ) 164 ) 165 self.terrain = bs.NodeActor( 166 bs.newnode( 167 'terrain', 168 attrs={ 169 'mesh': mesh, 170 'color_texture': color_texture, 171 'reflection': 'soft', 172 'reflection_scale': [0.3], 173 }, 174 ) 175 ) 176 self.trees = bs.NodeActor( 177 bs.newnode( 178 'terrain', 179 attrs={ 180 'mesh': trees_mesh, 181 'lighting': False, 182 'reflection': 'char', 183 'reflection_scale': [0.1], 184 'color_texture': trees_texture, 185 }, 186 ) 187 ) 188 self.bgterrain = bs.NodeActor( 189 bs.newnode( 190 'terrain', 191 attrs={ 192 'mesh': bgmesh, 193 'color': (0.92, 0.91, 0.9), 194 'lighting': False, 195 'background': True, 196 'color_texture': bgtex, 197 }, 198 ) 199 ) 200 201 self._update_timer = bs.Timer(1.0, self._update, repeat=True) 202 self._update() 203 204 # Hopefully this won't hitch but lets space these out anyway. 205 bs.add_clean_frame_callback(bs.WeakCall(self._start_preloads)) 206 207 random.seed() 208 209 # Need to update this for toolbar mode; currenly doesn't fit. 210 if bool(False): 211 if not (env.demo or env.arcade): 212 self._news = NewsDisplay(self) 213 214 self._attract_mode_timer = bs.Timer( 215 3.12, self._update_attract_mode, repeat=True 216 ) 217 218 app.classic.invoke_main_menu_ui() 219 220 app.classic.main_menu_did_initial_transition = True 221 222 def _update(self) -> None: 223 # pylint: disable=too-many-locals 224 # pylint: disable=too-many-statements 225 app = bs.app 226 env = app.env 227 assert app.classic is not None 228 229 # Update logo in case it changes. 230 if self._logo_node: 231 custom_texture = self._get_custom_logo_tex_name() 232 if custom_texture != self._custom_logo_tex_name: 233 self._custom_logo_tex_name = custom_texture 234 self._logo_node.texture = bs.gettexture( 235 custom_texture if custom_texture is not None else 'logo' 236 ) 237 self._logo_node.mesh_opaque = ( 238 None if custom_texture is not None else bs.getmesh('logo') 239 ) 240 self._logo_node.mesh_transparent = ( 241 None 242 if custom_texture is not None 243 else bs.getmesh('logoTransparent') 244 ) 245 246 # If language has changed, recreate our logo text/graphics. 247 lang = app.lang.language 248 if lang != self._language: 249 self._language = lang 250 y = -15 251 base_scale = 1.1 252 self._word_actors = [] 253 base_delay = 1.0 254 delay = base_delay 255 delay_inc = 0.02 256 257 # Come on faster after the first time. 258 if app.classic.main_menu_did_initial_transition: 259 base_delay = 0.0 260 delay = base_delay 261 delay_inc = 0.02 262 263 # We draw higher in kiosk mode (make sure to test this 264 # when making adjustments) for now we're hard-coded for 265 # a few languages.. should maybe look into generalizing this?.. 266 if app.lang.language == 'Chinese': 267 base_x = -270.0 268 x = base_x - 20.0 269 spacing = 85.0 * base_scale 270 y_extra = 0.0 if (env.demo or env.arcade) else 0.0 271 self._make_logo( 272 x - 110 + 50, 273 113 + y + 1.2 * y_extra, 274 0.34 * base_scale, 275 delay=base_delay + 0.1, 276 custom_texture='chTitleChar1', 277 jitter_scale=2.0, 278 vr_depth_offset=-30, 279 ) 280 x += spacing 281 delay += delay_inc 282 self._make_logo( 283 x - 10 + 50, 284 110 + y + 1.2 * y_extra, 285 0.31 * base_scale, 286 delay=base_delay + 0.15, 287 custom_texture='chTitleChar2', 288 jitter_scale=2.0, 289 vr_depth_offset=-30, 290 ) 291 x += 2.0 * spacing 292 delay += delay_inc 293 self._make_logo( 294 x + 180 - 140, 295 110 + y + 1.2 * y_extra, 296 0.3 * base_scale, 297 delay=base_delay + 0.25, 298 custom_texture='chTitleChar3', 299 jitter_scale=2.0, 300 vr_depth_offset=-30, 301 ) 302 x += spacing 303 delay += delay_inc 304 self._make_logo( 305 x + 241 - 120, 306 110 + y + 1.2 * y_extra, 307 0.31 * base_scale, 308 delay=base_delay + 0.3, 309 custom_texture='chTitleChar4', 310 jitter_scale=2.0, 311 vr_depth_offset=-30, 312 ) 313 x += spacing 314 delay += delay_inc 315 self._make_logo( 316 x + 300 - 90, 317 105 + y + 1.2 * y_extra, 318 0.34 * base_scale, 319 delay=base_delay + 0.35, 320 custom_texture='chTitleChar5', 321 jitter_scale=2.0, 322 vr_depth_offset=-30, 323 ) 324 self._make_logo( 325 base_x + 155, 326 146 + y + 1.2 * y_extra, 327 0.28 * base_scale, 328 delay=base_delay + 0.2, 329 rotate=-7, 330 ) 331 else: 332 base_x = -170 333 x = base_x - 20 334 spacing = 55 * base_scale 335 y_extra = 0 if (env.demo or env.arcade) else 0 336 xv1 = x 337 delay1 = delay 338 for shadow in (True, False): 339 x = xv1 340 delay = delay1 341 self._make_word( 342 'B', 343 x - 50, 344 y - 14 + 0.8 * y_extra, 345 scale=1.3 * base_scale, 346 delay=delay, 347 vr_depth_offset=3, 348 shadow=shadow, 349 ) 350 x += spacing 351 delay += delay_inc 352 self._make_word( 353 'm', 354 x, 355 y + y_extra, 356 delay=delay, 357 scale=base_scale, 358 shadow=shadow, 359 ) 360 x += spacing * 1.25 361 delay += delay_inc 362 self._make_word( 363 'b', 364 x, 365 y + y_extra - 10, 366 delay=delay, 367 scale=1.1 * base_scale, 368 vr_depth_offset=5, 369 shadow=shadow, 370 ) 371 x += spacing * 0.85 372 delay += delay_inc 373 self._make_word( 374 'S', 375 x, 376 y - 15 + 0.8 * y_extra, 377 scale=1.35 * base_scale, 378 delay=delay, 379 vr_depth_offset=14, 380 shadow=shadow, 381 ) 382 x += spacing 383 delay += delay_inc 384 self._make_word( 385 'q', 386 x, 387 y + y_extra, 388 delay=delay, 389 scale=base_scale, 390 shadow=shadow, 391 ) 392 x += spacing * 0.9 393 delay += delay_inc 394 self._make_word( 395 'u', 396 x, 397 y + y_extra, 398 delay=delay, 399 scale=base_scale, 400 vr_depth_offset=7, 401 shadow=shadow, 402 ) 403 x += spacing * 0.9 404 delay += delay_inc 405 self._make_word( 406 'a', 407 x, 408 y + y_extra, 409 delay=delay, 410 scale=base_scale, 411 shadow=shadow, 412 ) 413 x += spacing * 0.64 414 delay += delay_inc 415 self._make_word( 416 'd', 417 x, 418 y + y_extra - 10, 419 delay=delay, 420 scale=1.1 * base_scale, 421 vr_depth_offset=6, 422 shadow=shadow, 423 ) 424 self._make_logo( 425 base_x - 28, 426 125 + y + 1.2 * y_extra, 427 0.32 * base_scale, 428 delay=base_delay, 429 ) 430 431 def _make_word( 432 self, 433 word: str, 434 x: float, 435 y: float, 436 scale: float = 1.0, 437 delay: float = 0.0, 438 vr_depth_offset: float = 0.0, 439 shadow: bool = False, 440 ) -> None: 441 # pylint: disable=too-many-branches 442 # pylint: disable=too-many-locals 443 # pylint: disable=too-many-statements 444 if shadow: 445 word_obj = bs.NodeActor( 446 bs.newnode( 447 'text', 448 attrs={ 449 'position': (x, y), 450 'big': True, 451 'color': (0.0, 0.0, 0.2, 0.08), 452 'tilt_translate': 0.09, 453 'opacity_scales_shadow': False, 454 'shadow': 0.2, 455 'vr_depth': -130, 456 'v_align': 'center', 457 'project_scale': 0.97 * scale, 458 'scale': 1.0, 459 'text': word, 460 }, 461 ) 462 ) 463 self._word_actors.append(word_obj) 464 else: 465 word_obj = bs.NodeActor( 466 bs.newnode( 467 'text', 468 attrs={ 469 'position': (x, y), 470 'big': True, 471 'color': (1.2, 1.15, 1.15, 1.0), 472 'tilt_translate': 0.11, 473 'shadow': 0.2, 474 'vr_depth': -40 + vr_depth_offset, 475 'v_align': 'center', 476 'project_scale': scale, 477 'scale': 1.0, 478 'text': word, 479 }, 480 ) 481 ) 482 self._word_actors.append(word_obj) 483 484 # Add a bit of stop-motion-y jitter to the logo (unless we're in 485 # VR mode in which case its best to leave things still). 486 if not bs.app.env.vr: 487 cmb: bs.Node | None 488 cmb2: bs.Node | None 489 if not shadow: 490 cmb = bs.newnode( 491 'combine', owner=word_obj.node, attrs={'size': 2} 492 ) 493 else: 494 cmb = None 495 if shadow: 496 cmb2 = bs.newnode( 497 'combine', owner=word_obj.node, attrs={'size': 2} 498 ) 499 else: 500 cmb2 = None 501 if not shadow: 502 assert cmb and word_obj.node 503 cmb.connectattr('output', word_obj.node, 'position') 504 if shadow: 505 assert cmb2 and word_obj.node 506 cmb2.connectattr('output', word_obj.node, 'position') 507 keys = {} 508 keys2 = {} 509 time_v = 0.0 510 for _i in range(10): 511 val = x + (random.random() - 0.5) * 0.8 512 val2 = x + (random.random() - 0.5) * 0.8 513 keys[time_v * self._ts] = val 514 keys2[time_v * self._ts] = val2 + 5 515 time_v += random.random() * 0.1 516 if cmb is not None: 517 bs.animate(cmb, 'input0', keys, loop=True) 518 if cmb2 is not None: 519 bs.animate(cmb2, 'input0', keys2, loop=True) 520 keys = {} 521 keys2 = {} 522 time_v = 0 523 for _i in range(10): 524 val = y + (random.random() - 0.5) * 0.8 525 val2 = y + (random.random() - 0.5) * 0.8 526 keys[time_v * self._ts] = val 527 keys2[time_v * self._ts] = val2 - 9 528 time_v += random.random() * 0.1 529 if cmb is not None: 530 bs.animate(cmb, 'input1', keys, loop=True) 531 if cmb2 is not None: 532 bs.animate(cmb2, 'input1', keys2, loop=True) 533 534 if not shadow: 535 assert word_obj.node 536 bs.animate( 537 word_obj.node, 538 'project_scale', 539 {delay: 0.0, delay + 0.1: scale * 1.1, delay + 0.2: scale}, 540 ) 541 else: 542 assert word_obj.node 543 bs.animate( 544 word_obj.node, 545 'project_scale', 546 {delay: 0.0, delay + 0.1: scale * 1.1, delay + 0.2: scale}, 547 ) 548 549 def _get_custom_logo_tex_name(self) -> str | None: 550 plus = bui.app.plus 551 assert plus is not None 552 553 if plus.get_v1_account_misc_read_val('easter', False): 554 return 'logoEaster' 555 return None 556 557 # Pop the logo and menu in. 558 def _make_logo( 559 self, 560 x: float, 561 y: float, 562 scale: float, 563 delay: float, 564 custom_texture: str | None = None, 565 jitter_scale: float = 1.0, 566 rotate: float = 0.0, 567 vr_depth_offset: float = 0.0, 568 ) -> None: 569 # pylint: disable=too-many-locals 570 571 # Temp easter goodness. 572 if custom_texture is None: 573 custom_texture = self._get_custom_logo_tex_name() 574 self._custom_logo_tex_name = custom_texture 575 ltex = bs.gettexture( 576 custom_texture if custom_texture is not None else 'logo' 577 ) 578 mopaque = None if custom_texture is not None else bs.getmesh('logo') 579 mtrans = ( 580 None 581 if custom_texture is not None 582 else bs.getmesh('logoTransparent') 583 ) 584 logo = bs.NodeActor( 585 bs.newnode( 586 'image', 587 attrs={ 588 'texture': ltex, 589 'mesh_opaque': mopaque, 590 'mesh_transparent': mtrans, 591 'vr_depth': -10 + vr_depth_offset, 592 'rotate': rotate, 593 'attach': 'center', 594 'tilt_translate': 0.21, 595 'absolute_scale': True, 596 }, 597 ) 598 ) 599 self._logo_node = logo.node 600 self._word_actors.append(logo) 601 602 # Add a bit of stop-motion-y jitter to the logo (unless we're in 603 # VR mode in which case its best to leave things still). 604 assert logo.node 605 if not bs.app.env.vr: 606 cmb = bs.newnode('combine', owner=logo.node, attrs={'size': 2}) 607 cmb.connectattr('output', logo.node, 'position') 608 keys = {} 609 time_v = 0.0 610 611 # Gen some random keys for that stop-motion-y look 612 for _i in range(10): 613 keys[time_v] = x + (random.random() - 0.5) * 0.7 * jitter_scale 614 time_v += random.random() * 0.1 615 bs.animate(cmb, 'input0', keys, loop=True) 616 keys = {} 617 time_v = 0.0 618 for _i in range(10): 619 keys[time_v * self._ts] = ( 620 y + (random.random() - 0.5) * 0.7 * jitter_scale 621 ) 622 time_v += random.random() * 0.1 623 bs.animate(cmb, 'input1', keys, loop=True) 624 else: 625 logo.node.position = (x, y) 626 627 cmb = bs.newnode('combine', owner=logo.node, attrs={'size': 2}) 628 629 keys = { 630 delay: 0.0, 631 delay + 0.1: 700.0 * scale, 632 delay + 0.2: 600.0 * scale, 633 } 634 bs.animate(cmb, 'input0', keys) 635 bs.animate(cmb, 'input1', keys) 636 cmb.connectattr('output', logo.node, 'scale') 637 638 def _start_preloads(self) -> None: 639 # FIXME: The func that calls us back doesn't save/restore state 640 # or check for a dead activity so we have to do that ourself. 641 if self.expired: 642 return 643 with self.context: 644 _preload1() 645 646 def _start_menu_music() -> None: 647 assert bs.app.classic is not None 648 bs.setmusic(bs.MusicType.MENU) 649 650 bui.apptimer(0.5, _start_menu_music) 651 652 def _update_attract_mode(self) -> None: 653 if bui.app.classic is None: 654 return 655 656 if not bui.app.config.resolve('Show Demos When Idle'): 657 return 658 659 threshold = 20.0 660 661 # If we're idle *and* have been in this activity for that long, 662 # flip over to our cpu demo. 663 if bui.get_input_idle_time() > threshold and bs.time() > threshold: 664 bui.app.classic.run_stress_test( 665 playlist_type='Random', 666 playlist_name='__default__', 667 player_count=8, 668 round_duration=20, 669 attract_mode=True, 670 )
Activity showing the rotating main menu bg stuff.
25 def __init__(self, settings: dict): 26 super().__init__(settings) 27 self._logo_node: bs.Node | None = None 28 self._custom_logo_tex_name: str | None = None 29 self._word_actors: list[bs.Actor] = [] 30 self.my_name: bs.NodeActor | None = None 31 self._host_is_navigating_text: bs.NodeActor | None = None 32 self.version: bs.NodeActor | None = None 33 self.beta_info: bs.NodeActor | None = None 34 self.beta_info_2: bs.NodeActor | None = None 35 self.bottom: bs.NodeActor | None = None 36 self.vr_bottom_fill: bs.NodeActor | None = None 37 self.vr_top_fill: bs.NodeActor | None = None 38 self.terrain: bs.NodeActor | None = None 39 self.trees: bs.NodeActor | None = None 40 self.bgterrain: bs.NodeActor | None = None 41 self._ts = 0.86 42 self._language: str | None = None 43 self._update_timer: bs.Timer | None = None 44 self._news: NewsDisplay | None = None 45 self._attract_mode_timer: bs.Timer | None = None
Creates an Activity in the current bascenev1.Session.
The activity will not be actually run until bascenev1.Session.setactivity is called. 'settings' should be a dict of key/value pairs specific to the activity.
Activities should preload as much of their media/etc as possible in their constructor, but none of it should actually be used until they are transitioned in.
47 @override 48 def on_transition_in(self) -> None: 49 # pylint: disable=too-many-locals 50 # pylint: disable=too-many-statements 51 super().on_transition_in() 52 random.seed(123) 53 app = bs.app 54 env = app.env 55 assert app.classic is not None 56 57 plus = bs.app.plus 58 assert plus is not None 59 60 # Throw up some text that only clients can see so they know that the 61 # host is navigating menus while they're just staring at an 62 # empty-ish screen. 63 tval = bs.Lstr( 64 resource='hostIsNavigatingMenusText', 65 subs=[('${HOST}', plus.get_v1_account_display_string())], 66 ) 67 self._host_is_navigating_text = bs.NodeActor( 68 bs.newnode( 69 'text', 70 attrs={ 71 'text': tval, 72 'client_only': True, 73 'position': (0, -200), 74 'flatness': 1.0, 75 'h_align': 'center', 76 }, 77 ) 78 ) 79 if ( 80 not app.classic.main_menu_did_initial_transition 81 and self.my_name is not None 82 ): 83 assert self.my_name.node 84 bs.animate(self.my_name.node, 'opacity', {2.3: 0, 3.0: 1.0}) 85 86 # Throw in test build info. 87 self.beta_info = self.beta_info_2 = None 88 if env.test: 89 pos = (230, -5) 90 self.beta_info = bs.NodeActor( 91 bs.newnode( 92 'text', 93 attrs={ 94 'v_attach': 'center', 95 'h_align': 'center', 96 'color': (1, 1, 1, 1), 97 'shadow': 0.5, 98 'flatness': 0.5, 99 'scale': 1, 100 'vr_depth': -60, 101 'position': pos, 102 'text': bs.Lstr(resource='testBuildText'), 103 }, 104 ) 105 ) 106 if not app.classic.main_menu_did_initial_transition: 107 assert self.beta_info.node 108 bs.animate(self.beta_info.node, 'opacity', {1.3: 0, 1.8: 1.0}) 109 110 mesh = bs.getmesh('thePadLevel') 111 trees_mesh = bs.getmesh('trees') 112 bottom_mesh = bs.getmesh('thePadLevelBottom') 113 color_texture = bs.gettexture('thePadLevelColor') 114 trees_texture = bs.gettexture('treesColor') 115 bgtex = bs.gettexture('menuBG') 116 bgmesh = bs.getmesh('thePadBG') 117 118 # Load these last since most platforms don't use them. 119 vr_bottom_fill_mesh = bs.getmesh('thePadVRFillBottom') 120 vr_top_fill_mesh = bs.getmesh('thePadVRFillTop') 121 122 gnode = self.globalsnode 123 gnode.camera_mode = 'rotate' 124 125 tint = (1.14, 1.1, 1.0) 126 gnode.tint = tint 127 gnode.ambient_color = (1.06, 1.04, 1.03) 128 gnode.vignette_outer = (0.45, 0.55, 0.54) 129 gnode.vignette_inner = (0.99, 0.98, 0.98) 130 131 self.bottom = bs.NodeActor( 132 bs.newnode( 133 'terrain', 134 attrs={ 135 'mesh': bottom_mesh, 136 'lighting': False, 137 'reflection': 'soft', 138 'reflection_scale': [0.45], 139 'color_texture': color_texture, 140 }, 141 ) 142 ) 143 self.vr_bottom_fill = bs.NodeActor( 144 bs.newnode( 145 'terrain', 146 attrs={ 147 'mesh': vr_bottom_fill_mesh, 148 'lighting': False, 149 'vr_only': True, 150 'color_texture': color_texture, 151 }, 152 ) 153 ) 154 self.vr_top_fill = bs.NodeActor( 155 bs.newnode( 156 'terrain', 157 attrs={ 158 'mesh': vr_top_fill_mesh, 159 'vr_only': True, 160 'lighting': False, 161 'color_texture': bgtex, 162 }, 163 ) 164 ) 165 self.terrain = bs.NodeActor( 166 bs.newnode( 167 'terrain', 168 attrs={ 169 'mesh': mesh, 170 'color_texture': color_texture, 171 'reflection': 'soft', 172 'reflection_scale': [0.3], 173 }, 174 ) 175 ) 176 self.trees = bs.NodeActor( 177 bs.newnode( 178 'terrain', 179 attrs={ 180 'mesh': trees_mesh, 181 'lighting': False, 182 'reflection': 'char', 183 'reflection_scale': [0.1], 184 'color_texture': trees_texture, 185 }, 186 ) 187 ) 188 self.bgterrain = bs.NodeActor( 189 bs.newnode( 190 'terrain', 191 attrs={ 192 'mesh': bgmesh, 193 'color': (0.92, 0.91, 0.9), 194 'lighting': False, 195 'background': True, 196 'color_texture': bgtex, 197 }, 198 ) 199 ) 200 201 self._update_timer = bs.Timer(1.0, self._update, repeat=True) 202 self._update() 203 204 # Hopefully this won't hitch but lets space these out anyway. 205 bs.add_clean_frame_callback(bs.WeakCall(self._start_preloads)) 206 207 random.seed() 208 209 # Need to update this for toolbar mode; currenly doesn't fit. 210 if bool(False): 211 if not (env.demo or env.arcade): 212 self._news = NewsDisplay(self) 213 214 self._attract_mode_timer = bs.Timer( 215 3.12, self._update_attract_mode, repeat=True 216 ) 217 218 app.classic.invoke_main_menu_ui() 219 220 app.classic.main_menu_did_initial_transition = True
Called when the Activity is first becoming visible.
Upon this call, the Activity should fade in backgrounds, start playing music, etc. It does not yet have access to players or teams, however. They remain owned by the previous Activity up until bascenev1.Activity.on_begin() is called.
Inherited Members
- bascenev1._activity.Activity
- settings_raw
- teams
- players
- announce_player_deaths
- is_joining_activity
- allow_pausing
- allow_kick_idle_players
- use_fixed_vr_overlay
- slow_motion
- inherits_slow_motion
- inherits_music
- inherits_vr_camera_offset
- inherits_vr_overlay_center
- inherits_tint
- allow_mid_activity_joins
- transition_time
- can_show_ad_on_death
- paused_text
- preloads
- lobby
- context
- globalsnode
- stats
- on_expire
- customdata
- expired
- playertype
- teamtype
- retain_actor
- add_actor_weak_ref
- session
- on_player_join
- on_player_leave
- on_team_join
- on_team_leave
- on_transition_out
- on_begin
- handlemessage
- has_transitioned_in
- has_begun
- has_ended
- is_transitioning_out
- transition_out
- end
- create_player
- create_team
- bascenev1._dependency.DependencyComponent
- dep_is_present
- get_dynamic_deps
673class NewsDisplay: 674 """Wrangles news display.""" 675 676 def __init__(self, activity: bs.Activity): 677 self._valid = True 678 self._message_duration = 10.0 679 self._message_spacing = 2.0 680 self._text: bs.NodeActor | None = None 681 self._activity = weakref.ref(activity) 682 self._phrases: list[str] = [] 683 self._used_phrases: list[str] = [] 684 self._phrase_change_timer: bs.Timer | None = None 685 686 # If we're signed in, fetch news immediately. Otherwise wait 687 # until we are signed in. 688 self._fetch_timer: bs.Timer | None = bs.Timer( 689 1.0, bs.WeakCall(self._try_fetching_news), repeat=True 690 ) 691 self._try_fetching_news() 692 693 # We now want to wait until we're signed in before fetching news. 694 def _try_fetching_news(self) -> None: 695 plus = bui.app.plus 696 assert plus is not None 697 698 if plus.get_v1_account_state() == 'signed_in': 699 self._fetch_news() 700 self._fetch_timer = None 701 702 def _fetch_news(self) -> None: 703 plus = bui.app.plus 704 assert plus is not None 705 706 assert bs.app.classic is not None 707 bs.app.classic.main_menu_last_news_fetch_time = time.time() 708 709 # UPDATE - We now just pull news from MRVs. 710 news = plus.get_v1_account_misc_read_val('n', None) 711 if news is not None: 712 self._got_news(news) 713 714 def _change_phrase(self) -> None: 715 from bascenev1lib.actor.text import Text 716 717 app = bs.app 718 assert app.classic is not None 719 720 # If our news is way out of date, lets re-request it; otherwise, 721 # rotate our phrase. 722 assert app.classic.main_menu_last_news_fetch_time is not None 723 if time.time() - app.classic.main_menu_last_news_fetch_time > 600.0: 724 self._fetch_news() 725 self._text = None 726 else: 727 if self._text is not None: 728 if not self._phrases: 729 for phr in self._used_phrases: 730 self._phrases.insert(0, phr) 731 val = self._phrases.pop() 732 if val == '__ACH__': 733 vrmode = app.env.vr 734 Text( 735 bs.Lstr(resource='nextAchievementsText'), 736 color=((1, 1, 1, 1) if vrmode else (0.95, 0.9, 1, 0.4)), 737 host_only=True, 738 maxwidth=200, 739 position=(-300, -35), 740 h_align=Text.HAlign.RIGHT, 741 transition=Text.Transition.FADE_IN, 742 scale=0.9 if vrmode else 0.7, 743 flatness=1.0 if vrmode else 0.6, 744 shadow=1.0 if vrmode else 0.5, 745 h_attach=Text.HAttach.CENTER, 746 v_attach=Text.VAttach.TOP, 747 transition_delay=1.0, 748 transition_out_delay=self._message_duration, 749 ).autoretain() 750 achs = [ 751 a 752 for a in app.classic.ach.achievements 753 if not a.complete 754 ] 755 if achs: 756 ach = achs.pop(random.randrange(min(4, len(achs)))) 757 ach.create_display( 758 -180, 759 -35, 760 1.0, 761 outdelay=self._message_duration, 762 style='news', 763 ) 764 if achs: 765 ach = achs.pop(random.randrange(min(8, len(achs)))) 766 ach.create_display( 767 180, 768 -35, 769 1.25, 770 outdelay=self._message_duration, 771 style='news', 772 ) 773 else: 774 spc = self._message_spacing 775 keys = { 776 spc: 0.0, 777 spc + 1.0: 1.0, 778 spc + self._message_duration - 1.0: 1.0, 779 spc + self._message_duration: 0.0, 780 } 781 assert self._text.node 782 bs.animate(self._text.node, 'opacity', keys) 783 # {k: v 784 # for k, v in list(keys.items())}) 785 self._text.node.text = val 786 787 def _got_news(self, news: str) -> None: 788 # Run this stuff in the context of our activity since we need to 789 # make nodes and stuff.. should fix the serverget call so it. 790 activity = self._activity() 791 if activity is None or activity.expired: 792 return 793 with activity.context: 794 self._phrases.clear() 795 796 # Show upcoming achievements in non-vr versions (currently 797 # too hard to read in vr). 798 self._used_phrases = (['__ACH__'] if not bs.app.env.vr else []) + [ 799 s for s in news.split('<br>\n') if s != '' 800 ] 801 self._phrase_change_timer = bs.Timer( 802 (self._message_duration + self._message_spacing), 803 bs.WeakCall(self._change_phrase), 804 repeat=True, 805 ) 806 807 assert bs.app.classic is not None 808 scl = ( 809 1.2 810 if (bs.app.ui_v1.uiscale is bs.UIScale.SMALL or bs.app.env.vr) 811 else 0.8 812 ) 813 814 color2 = (1, 1, 1, 1) if bs.app.env.vr else (0.7, 0.65, 0.75, 1.0) 815 shadow = 1.0 if bs.app.env.vr else 0.4 816 self._text = bs.NodeActor( 817 bs.newnode( 818 'text', 819 attrs={ 820 'v_attach': 'top', 821 'h_attach': 'center', 822 'h_align': 'center', 823 'vr_depth': -20, 824 'shadow': shadow, 825 'flatness': 0.8, 826 'v_align': 'top', 827 'color': color2, 828 'scale': scl, 829 'maxwidth': 900.0 / scl, 830 'position': (0, -10), 831 }, 832 ) 833 ) 834 self._change_phrase()
Wrangles news display.
676 def __init__(self, activity: bs.Activity): 677 self._valid = True 678 self._message_duration = 10.0 679 self._message_spacing = 2.0 680 self._text: bs.NodeActor | None = None 681 self._activity = weakref.ref(activity) 682 self._phrases: list[str] = [] 683 self._used_phrases: list[str] = [] 684 self._phrase_change_timer: bs.Timer | None = None 685 686 # If we're signed in, fetch news immediately. Otherwise wait 687 # until we are signed in. 688 self._fetch_timer: bs.Timer | None = bs.Timer( 689 1.0, bs.WeakCall(self._try_fetching_news), repeat=True 690 ) 691 self._try_fetching_news()
937class MainMenuSession(bs.Session): 938 """Session that runs the main menu environment.""" 939 940 def __init__(self) -> None: 941 # Gather dependencies we'll need (just our activity). 942 self._activity_deps = bs.DependencySet(bs.Dependency(MainMenuActivity)) 943 944 super().__init__([self._activity_deps]) 945 self._locked = False 946 self.setactivity(bs.newactivity(MainMenuActivity)) 947 948 @override 949 def on_activity_end(self, activity: bs.Activity, results: Any) -> None: 950 if self._locked: 951 bui.unlock_all_input() 952 953 # Any ending activity leads us into the main menu one. 954 self.setactivity(bs.newactivity(MainMenuActivity)) 955 956 @override 957 def on_player_request(self, player: bs.SessionPlayer) -> bool: 958 # Reject all player requests. 959 return False
Session that runs the main menu environment.
940 def __init__(self) -> None: 941 # Gather dependencies we'll need (just our activity). 942 self._activity_deps = bs.DependencySet(bs.Dependency(MainMenuActivity)) 943 944 super().__init__([self._activity_deps]) 945 self._locked = False 946 self.setactivity(bs.newactivity(MainMenuActivity))
Instantiate a session.
depsets should be a sequence of successfully resolved bascenev1.DependencySet instances; one for each bascenev1.Activity the session may potentially run.
948 @override 949 def on_activity_end(self, activity: bs.Activity, results: Any) -> None: 950 if self._locked: 951 bui.unlock_all_input() 952 953 # Any ending activity leads us into the main menu one. 954 self.setactivity(bs.newactivity(MainMenuActivity))
Called when the current bascenev1.Activity has ended.
The bascenev1.Session should look at the results and start another bascenev1.Activity.
956 @override 957 def on_player_request(self, player: bs.SessionPlayer) -> bool: 958 # Reject all player requests. 959 return False
Called when a new bascenev1.Player wants to join the Session.
This should return True or False to accept/reject.
Inherited Members
- bascenev1._session.Session
- use_teams
- use_team_colors
- lobby
- max_players
- min_players
- sessionplayers
- customdata
- sessionteams
- tournament_id
- submit_score
- stats
- context
- sessionglobalsnode
- should_allow_mid_activity_joins
- on_player_leave
- end
- on_team_join
- on_team_leave
- end_activity
- handlemessage
- setactivity
- getactivity
- begin_next_activity