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