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_attrs = { 585 'position': (x, y), 586 'texture': ltex, 587 'mesh_opaque': mopaque, 588 'mesh_transparent': mtrans, 589 'vr_depth': -10 + vr_depth_offset, 590 'rotate': rotate, 591 'attach': 'center', 592 'tilt_translate': 0.21, 593 'absolute_scale': True, 594 } 595 if custom_texture is None: 596 logo_attrs['scale'] = (2000.0, 2000.0) 597 logo = bs.NodeActor(bs.newnode('image', attrs=logo_attrs)) 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 605 def jitter() -> None: 606 if not bs.app.env.vr: 607 cmb = bs.newnode('combine', owner=logo.node, attrs={'size': 2}) 608 cmb.connectattr('output', logo.node, 'position') 609 keys = {} 610 time_v = 0.0 611 612 # Gen some random keys for that stop-motion-y look 613 for _i in range(10): 614 keys[time_v] = ( 615 x + (random.random() - 0.5) * 0.7 * jitter_scale 616 ) 617 time_v += random.random() * 0.1 618 bs.animate(cmb, 'input0', keys, loop=True) 619 keys = {} 620 time_v = 0.0 621 for _i in range(10): 622 keys[time_v * self._ts] = ( 623 y + (random.random() - 0.5) * 0.7 * jitter_scale 624 ) 625 time_v += random.random() * 0.1 626 bs.animate(cmb, 'input1', keys, loop=True) 627 628 # Do a fun spinny animation on the logo the first time in. 629 if ( 630 custom_texture is None 631 and bs.app.classic is not None 632 and not bs.app.classic.main_menu_did_initial_transition 633 ): 634 jitter() 635 cmb = bs.newnode('combine', owner=logo.node, attrs={'size': 2}) 636 637 delay = 0.0 638 keys = { 639 delay: 5000.0 * scale, 640 delay + 0.4: 530.0 * scale, 641 delay + 0.45: 620.0 * scale, 642 delay + 0.5: 590.0 * scale, 643 delay + 0.55: 605.0 * scale, 644 delay + 0.6: 600.0 * scale, 645 } 646 bs.animate(cmb, 'input0', keys) 647 bs.animate(cmb, 'input1', keys) 648 cmb.connectattr('output', logo.node, 'scale') 649 650 keys = { 651 delay: 100.0, 652 delay + 0.4: 370.0, 653 delay + 0.45: 357.0, 654 delay + 0.5: 360.0, 655 } 656 bs.animate(logo.node, 'rotate', keys) 657 else: 658 # For all other cases do a simple scale up animation. 659 jitter() 660 cmb = bs.newnode('combine', owner=logo.node, attrs={'size': 2}) 661 662 keys = { 663 delay: 0.0, 664 delay + 0.1: 700.0 * scale, 665 delay + 0.2: 600.0 * scale, 666 } 667 bs.animate(cmb, 'input0', keys) 668 bs.animate(cmb, 'input1', keys) 669 cmb.connectattr('output', logo.node, 'scale') 670 671 def _start_preloads(self) -> None: 672 # FIXME: The func that calls us back doesn't save/restore state 673 # or check for a dead activity so we have to do that ourself. 674 if self.expired: 675 return 676 with self.context: 677 _preload1() 678 679 def _start_menu_music() -> None: 680 assert bs.app.classic is not None 681 bs.setmusic(bs.MusicType.MENU) 682 683 bui.apptimer(0.5, _start_menu_music) 684 685 def _update_attract_mode(self) -> None: 686 if bui.app.classic is None: 687 return 688 689 if not bui.app.config.resolve('Show Demos When Idle'): 690 return 691 692 threshold = 20.0 693 694 # If we're idle *and* have been in this activity for that long, 695 # flip over to our cpu demo. 696 if bui.get_input_idle_time() > threshold and bs.time() > threshold: 697 bui.app.classic.run_stress_test( 698 playlist_type='Random', 699 playlist_name='__default__', 700 player_count=8, 701 round_duration=20, 702 attract_mode=True, 703 ) 704 705 706class NewsDisplay: 707 """Wrangles news display.""" 708 709 def __init__(self, activity: bs.Activity): 710 self._valid = True 711 self._message_duration = 10.0 712 self._message_spacing = 2.0 713 self._text: bs.NodeActor | None = None 714 self._activity = weakref.ref(activity) 715 self._phrases: list[str] = [] 716 self._used_phrases: list[str] = [] 717 self._phrase_change_timer: bs.Timer | None = None 718 719 # If we're signed in, fetch news immediately. Otherwise wait 720 # until we are signed in. 721 self._fetch_timer: bs.Timer | None = bs.Timer( 722 1.0, bs.WeakCall(self._try_fetching_news), repeat=True 723 ) 724 self._try_fetching_news() 725 726 # We now want to wait until we're signed in before fetching news. 727 def _try_fetching_news(self) -> None: 728 plus = bui.app.plus 729 assert plus is not None 730 731 if plus.get_v1_account_state() == 'signed_in': 732 self._fetch_news() 733 self._fetch_timer = None 734 735 def _fetch_news(self) -> None: 736 plus = bui.app.plus 737 assert plus is not None 738 739 assert bs.app.classic is not None 740 bs.app.classic.main_menu_last_news_fetch_time = time.time() 741 742 # UPDATE - We now just pull news from MRVs. 743 news = plus.get_v1_account_misc_read_val('n', None) 744 if news is not None: 745 self._got_news(news) 746 747 def _change_phrase(self) -> None: 748 from bascenev1lib.actor.text import Text 749 750 app = bs.app 751 assert app.classic is not None 752 753 # If our news is way out of date, lets re-request it; otherwise, 754 # rotate our phrase. 755 assert app.classic.main_menu_last_news_fetch_time is not None 756 if time.time() - app.classic.main_menu_last_news_fetch_time > 600.0: 757 self._fetch_news() 758 self._text = None 759 else: 760 if self._text is not None: 761 if not self._phrases: 762 for phr in self._used_phrases: 763 self._phrases.insert(0, phr) 764 val = self._phrases.pop() 765 if val == '__ACH__': 766 vrmode = app.env.vr 767 Text( 768 bs.Lstr(resource='nextAchievementsText'), 769 color=((1, 1, 1, 1) if vrmode else (0.95, 0.9, 1, 0.4)), 770 host_only=True, 771 maxwidth=200, 772 position=(-300, -35), 773 h_align=Text.HAlign.RIGHT, 774 transition=Text.Transition.FADE_IN, 775 scale=0.9 if vrmode else 0.7, 776 flatness=1.0 if vrmode else 0.6, 777 shadow=1.0 if vrmode else 0.5, 778 h_attach=Text.HAttach.CENTER, 779 v_attach=Text.VAttach.TOP, 780 transition_delay=1.0, 781 transition_out_delay=self._message_duration, 782 ).autoretain() 783 achs = [ 784 a 785 for a in app.classic.ach.achievements 786 if not a.complete 787 ] 788 if achs: 789 ach = achs.pop(random.randrange(min(4, len(achs)))) 790 ach.create_display( 791 -180, 792 -35, 793 1.0, 794 outdelay=self._message_duration, 795 style='news', 796 ) 797 if achs: 798 ach = achs.pop(random.randrange(min(8, len(achs)))) 799 ach.create_display( 800 180, 801 -35, 802 1.25, 803 outdelay=self._message_duration, 804 style='news', 805 ) 806 else: 807 spc = self._message_spacing 808 keys = { 809 spc: 0.0, 810 spc + 1.0: 1.0, 811 spc + self._message_duration - 1.0: 1.0, 812 spc + self._message_duration: 0.0, 813 } 814 assert self._text.node 815 bs.animate(self._text.node, 'opacity', keys) 816 # {k: v 817 # for k, v in list(keys.items())}) 818 self._text.node.text = val 819 820 def _got_news(self, news: str) -> None: 821 # Run this stuff in the context of our activity since we need to 822 # make nodes and stuff.. should fix the serverget call so it. 823 activity = self._activity() 824 if activity is None or activity.expired: 825 return 826 with activity.context: 827 self._phrases.clear() 828 829 # Show upcoming achievements in non-vr versions (currently 830 # too hard to read in vr). 831 self._used_phrases = (['__ACH__'] if not bs.app.env.vr else []) + [ 832 s for s in news.split('<br>\n') if s != '' 833 ] 834 self._phrase_change_timer = bs.Timer( 835 (self._message_duration + self._message_spacing), 836 bs.WeakCall(self._change_phrase), 837 repeat=True, 838 ) 839 840 assert bs.app.classic is not None 841 scl = ( 842 1.2 843 if (bs.app.ui_v1.uiscale is bs.UIScale.SMALL or bs.app.env.vr) 844 else 0.8 845 ) 846 847 color2 = (1, 1, 1, 1) if bs.app.env.vr else (0.7, 0.65, 0.75, 1.0) 848 shadow = 1.0 if bs.app.env.vr else 0.4 849 self._text = bs.NodeActor( 850 bs.newnode( 851 'text', 852 attrs={ 853 'v_attach': 'top', 854 'h_attach': 'center', 855 'h_align': 'center', 856 'vr_depth': -20, 857 'shadow': shadow, 858 'flatness': 0.8, 859 'v_align': 'top', 860 'color': color2, 861 'scale': scl, 862 'maxwidth': 900.0 / scl, 863 'position': (0, -10), 864 }, 865 ) 866 ) 867 self._change_phrase() 868 869 870def _preload1() -> None: 871 """Pre-load some assets a second or two into the main menu. 872 873 Helps avoid hitches later on. 874 """ 875 for mname in [ 876 'plasticEyesTransparent', 877 'playerLineup1Transparent', 878 'playerLineup2Transparent', 879 'playerLineup3Transparent', 880 'playerLineup4Transparent', 881 'angryComputerTransparent', 882 'scrollWidgetShort', 883 'windowBGBlotch', 884 ]: 885 bs.getmesh(mname) 886 for tname in ['playerLineup', 'lock']: 887 bs.gettexture(tname) 888 for tex in [ 889 'iconRunaround', 890 'iconOnslaught', 891 'medalComplete', 892 'medalBronze', 893 'medalSilver', 894 'medalGold', 895 'characterIconMask', 896 ]: 897 bs.gettexture(tex) 898 bs.gettexture('bg') 899 from bascenev1lib.actor.powerupbox import PowerupBoxFactory 900 901 PowerupBoxFactory.get() 902 bui.apptimer(0.1, _preload2) 903 904 905def _preload2() -> None: 906 # FIXME: Could integrate these loads with the classes that use them 907 # so they don't have to redundantly call the load 908 # (even if the actual result is cached). 909 for mname in ['powerup', 'powerupSimple']: 910 bs.getmesh(mname) 911 for tname in [ 912 'powerupBomb', 913 'powerupSpeed', 914 'powerupPunch', 915 'powerupIceBombs', 916 'powerupStickyBombs', 917 'powerupShield', 918 'powerupImpactBombs', 919 'powerupHealth', 920 ]: 921 bs.gettexture(tname) 922 for sname in [ 923 'powerup01', 924 'boxDrop', 925 'boxingBell', 926 'scoreHit01', 927 'scoreHit02', 928 'dripity', 929 'spawn', 930 'gong', 931 ]: 932 bs.getsound(sname) 933 from bascenev1lib.actor.bomb import BombFactory 934 935 BombFactory.get() 936 bui.apptimer(0.1, _preload3) 937 938 939def _preload3() -> None: 940 from bascenev1lib.actor.spazfactory import SpazFactory 941 942 for mname in ['bomb', 'bombSticky', 'impactBomb']: 943 bs.getmesh(mname) 944 for tname in [ 945 'bombColor', 946 'bombColorIce', 947 'bombStickyColor', 948 'impactBombColor', 949 'impactBombColorLit', 950 ]: 951 bs.gettexture(tname) 952 for sname in ['freeze', 'fuse01', 'activateBeep', 'warnBeep']: 953 bs.getsound(sname) 954 SpazFactory.get() 955 bui.apptimer(0.2, _preload4) 956 957 958def _preload4() -> None: 959 for tname in ['bar', 'meter', 'null', 'flagColor', 'achievementOutline']: 960 bs.gettexture(tname) 961 for mname in ['frameInset', 'meterTransparent', 'achievementOutline']: 962 bs.getmesh(mname) 963 for sname in ['metalHit', 'metalSkid', 'refWhistle', 'achievement']: 964 bs.getsound(sname) 965 from bascenev1lib.actor.flag import FlagFactory 966 967 FlagFactory.get() 968 969 970class MainMenuSession(bs.Session): 971 """Session that runs the main menu environment.""" 972 973 def __init__(self) -> None: 974 # Gather dependencies we'll need (just our activity). 975 self._activity_deps = bs.DependencySet(bs.Dependency(MainMenuActivity)) 976 977 super().__init__([self._activity_deps]) 978 self._locked = False 979 self.setactivity(bs.newactivity(MainMenuActivity)) 980 981 @override 982 def on_activity_end(self, activity: bs.Activity, results: Any) -> None: 983 if self._locked: 984 bui.unlock_all_input() 985 986 # Any ending activity leads us into the main menu one. 987 self.setactivity(bs.newactivity(MainMenuActivity)) 988 989 @override 990 def on_player_request(self, player: bs.SessionPlayer) -> bool: 991 # Reject all player requests. 992 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_attrs = { 586 'position': (x, y), 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 if custom_texture is None: 597 logo_attrs['scale'] = (2000.0, 2000.0) 598 logo = bs.NodeActor(bs.newnode('image', attrs=logo_attrs)) 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 606 def jitter() -> None: 607 if not bs.app.env.vr: 608 cmb = bs.newnode('combine', owner=logo.node, attrs={'size': 2}) 609 cmb.connectattr('output', logo.node, 'position') 610 keys = {} 611 time_v = 0.0 612 613 # Gen some random keys for that stop-motion-y look 614 for _i in range(10): 615 keys[time_v] = ( 616 x + (random.random() - 0.5) * 0.7 * jitter_scale 617 ) 618 time_v += random.random() * 0.1 619 bs.animate(cmb, 'input0', keys, loop=True) 620 keys = {} 621 time_v = 0.0 622 for _i in range(10): 623 keys[time_v * self._ts] = ( 624 y + (random.random() - 0.5) * 0.7 * jitter_scale 625 ) 626 time_v += random.random() * 0.1 627 bs.animate(cmb, 'input1', keys, loop=True) 628 629 # Do a fun spinny animation on the logo the first time in. 630 if ( 631 custom_texture is None 632 and bs.app.classic is not None 633 and not bs.app.classic.main_menu_did_initial_transition 634 ): 635 jitter() 636 cmb = bs.newnode('combine', owner=logo.node, attrs={'size': 2}) 637 638 delay = 0.0 639 keys = { 640 delay: 5000.0 * scale, 641 delay + 0.4: 530.0 * scale, 642 delay + 0.45: 620.0 * scale, 643 delay + 0.5: 590.0 * scale, 644 delay + 0.55: 605.0 * scale, 645 delay + 0.6: 600.0 * scale, 646 } 647 bs.animate(cmb, 'input0', keys) 648 bs.animate(cmb, 'input1', keys) 649 cmb.connectattr('output', logo.node, 'scale') 650 651 keys = { 652 delay: 100.0, 653 delay + 0.4: 370.0, 654 delay + 0.45: 357.0, 655 delay + 0.5: 360.0, 656 } 657 bs.animate(logo.node, 'rotate', keys) 658 else: 659 # For all other cases do a simple scale up animation. 660 jitter() 661 cmb = bs.newnode('combine', owner=logo.node, attrs={'size': 2}) 662 663 keys = { 664 delay: 0.0, 665 delay + 0.1: 700.0 * scale, 666 delay + 0.2: 600.0 * scale, 667 } 668 bs.animate(cmb, 'input0', keys) 669 bs.animate(cmb, 'input1', keys) 670 cmb.connectattr('output', logo.node, 'scale') 671 672 def _start_preloads(self) -> None: 673 # FIXME: The func that calls us back doesn't save/restore state 674 # or check for a dead activity so we have to do that ourself. 675 if self.expired: 676 return 677 with self.context: 678 _preload1() 679 680 def _start_menu_music() -> None: 681 assert bs.app.classic is not None 682 bs.setmusic(bs.MusicType.MENU) 683 684 bui.apptimer(0.5, _start_menu_music) 685 686 def _update_attract_mode(self) -> None: 687 if bui.app.classic is None: 688 return 689 690 if not bui.app.config.resolve('Show Demos When Idle'): 691 return 692 693 threshold = 20.0 694 695 # If we're idle *and* have been in this activity for that long, 696 # flip over to our cpu demo. 697 if bui.get_input_idle_time() > threshold and bs.time() > threshold: 698 bui.app.classic.run_stress_test( 699 playlist_type='Random', 700 playlist_name='__default__', 701 player_count=8, 702 round_duration=20, 703 attract_mode=True, 704 )
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.
707class NewsDisplay: 708 """Wrangles news display.""" 709 710 def __init__(self, activity: bs.Activity): 711 self._valid = True 712 self._message_duration = 10.0 713 self._message_spacing = 2.0 714 self._text: bs.NodeActor | None = None 715 self._activity = weakref.ref(activity) 716 self._phrases: list[str] = [] 717 self._used_phrases: list[str] = [] 718 self._phrase_change_timer: bs.Timer | None = None 719 720 # If we're signed in, fetch news immediately. Otherwise wait 721 # until we are signed in. 722 self._fetch_timer: bs.Timer | None = bs.Timer( 723 1.0, bs.WeakCall(self._try_fetching_news), repeat=True 724 ) 725 self._try_fetching_news() 726 727 # We now want to wait until we're signed in before fetching news. 728 def _try_fetching_news(self) -> None: 729 plus = bui.app.plus 730 assert plus is not None 731 732 if plus.get_v1_account_state() == 'signed_in': 733 self._fetch_news() 734 self._fetch_timer = None 735 736 def _fetch_news(self) -> None: 737 plus = bui.app.plus 738 assert plus is not None 739 740 assert bs.app.classic is not None 741 bs.app.classic.main_menu_last_news_fetch_time = time.time() 742 743 # UPDATE - We now just pull news from MRVs. 744 news = plus.get_v1_account_misc_read_val('n', None) 745 if news is not None: 746 self._got_news(news) 747 748 def _change_phrase(self) -> None: 749 from bascenev1lib.actor.text import Text 750 751 app = bs.app 752 assert app.classic is not None 753 754 # If our news is way out of date, lets re-request it; otherwise, 755 # rotate our phrase. 756 assert app.classic.main_menu_last_news_fetch_time is not None 757 if time.time() - app.classic.main_menu_last_news_fetch_time > 600.0: 758 self._fetch_news() 759 self._text = None 760 else: 761 if self._text is not None: 762 if not self._phrases: 763 for phr in self._used_phrases: 764 self._phrases.insert(0, phr) 765 val = self._phrases.pop() 766 if val == '__ACH__': 767 vrmode = app.env.vr 768 Text( 769 bs.Lstr(resource='nextAchievementsText'), 770 color=((1, 1, 1, 1) if vrmode else (0.95, 0.9, 1, 0.4)), 771 host_only=True, 772 maxwidth=200, 773 position=(-300, -35), 774 h_align=Text.HAlign.RIGHT, 775 transition=Text.Transition.FADE_IN, 776 scale=0.9 if vrmode else 0.7, 777 flatness=1.0 if vrmode else 0.6, 778 shadow=1.0 if vrmode else 0.5, 779 h_attach=Text.HAttach.CENTER, 780 v_attach=Text.VAttach.TOP, 781 transition_delay=1.0, 782 transition_out_delay=self._message_duration, 783 ).autoretain() 784 achs = [ 785 a 786 for a in app.classic.ach.achievements 787 if not a.complete 788 ] 789 if achs: 790 ach = achs.pop(random.randrange(min(4, len(achs)))) 791 ach.create_display( 792 -180, 793 -35, 794 1.0, 795 outdelay=self._message_duration, 796 style='news', 797 ) 798 if achs: 799 ach = achs.pop(random.randrange(min(8, len(achs)))) 800 ach.create_display( 801 180, 802 -35, 803 1.25, 804 outdelay=self._message_duration, 805 style='news', 806 ) 807 else: 808 spc = self._message_spacing 809 keys = { 810 spc: 0.0, 811 spc + 1.0: 1.0, 812 spc + self._message_duration - 1.0: 1.0, 813 spc + self._message_duration: 0.0, 814 } 815 assert self._text.node 816 bs.animate(self._text.node, 'opacity', keys) 817 # {k: v 818 # for k, v in list(keys.items())}) 819 self._text.node.text = val 820 821 def _got_news(self, news: str) -> None: 822 # Run this stuff in the context of our activity since we need to 823 # make nodes and stuff.. should fix the serverget call so it. 824 activity = self._activity() 825 if activity is None or activity.expired: 826 return 827 with activity.context: 828 self._phrases.clear() 829 830 # Show upcoming achievements in non-vr versions (currently 831 # too hard to read in vr). 832 self._used_phrases = (['__ACH__'] if not bs.app.env.vr else []) + [ 833 s for s in news.split('<br>\n') if s != '' 834 ] 835 self._phrase_change_timer = bs.Timer( 836 (self._message_duration + self._message_spacing), 837 bs.WeakCall(self._change_phrase), 838 repeat=True, 839 ) 840 841 assert bs.app.classic is not None 842 scl = ( 843 1.2 844 if (bs.app.ui_v1.uiscale is bs.UIScale.SMALL or bs.app.env.vr) 845 else 0.8 846 ) 847 848 color2 = (1, 1, 1, 1) if bs.app.env.vr else (0.7, 0.65, 0.75, 1.0) 849 shadow = 1.0 if bs.app.env.vr else 0.4 850 self._text = bs.NodeActor( 851 bs.newnode( 852 'text', 853 attrs={ 854 'v_attach': 'top', 855 'h_attach': 'center', 856 'h_align': 'center', 857 'vr_depth': -20, 858 'shadow': shadow, 859 'flatness': 0.8, 860 'v_align': 'top', 861 'color': color2, 862 'scale': scl, 863 'maxwidth': 900.0 / scl, 864 'position': (0, -10), 865 }, 866 ) 867 ) 868 self._change_phrase()
Wrangles news display.
710 def __init__(self, activity: bs.Activity): 711 self._valid = True 712 self._message_duration = 10.0 713 self._message_spacing = 2.0 714 self._text: bs.NodeActor | None = None 715 self._activity = weakref.ref(activity) 716 self._phrases: list[str] = [] 717 self._used_phrases: list[str] = [] 718 self._phrase_change_timer: bs.Timer | None = None 719 720 # If we're signed in, fetch news immediately. Otherwise wait 721 # until we are signed in. 722 self._fetch_timer: bs.Timer | None = bs.Timer( 723 1.0, bs.WeakCall(self._try_fetching_news), repeat=True 724 ) 725 self._try_fetching_news()
971class MainMenuSession(bs.Session): 972 """Session that runs the main menu environment.""" 973 974 def __init__(self) -> None: 975 # Gather dependencies we'll need (just our activity). 976 self._activity_deps = bs.DependencySet(bs.Dependency(MainMenuActivity)) 977 978 super().__init__([self._activity_deps]) 979 self._locked = False 980 self.setactivity(bs.newactivity(MainMenuActivity)) 981 982 @override 983 def on_activity_end(self, activity: bs.Activity, results: Any) -> None: 984 if self._locked: 985 bui.unlock_all_input() 986 987 # Any ending activity leads us into the main menu one. 988 self.setactivity(bs.newactivity(MainMenuActivity)) 989 990 @override 991 def on_player_request(self, player: bs.SessionPlayer) -> bool: 992 # Reject all player requests. 993 return False
Session that runs the main menu environment.
974 def __init__(self) -> None: 975 # Gather dependencies we'll need (just our activity). 976 self._activity_deps = bs.DependencySet(bs.Dependency(MainMenuActivity)) 977 978 super().__init__([self._activity_deps]) 979 self._locked = False 980 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.
982 @override 983 def on_activity_end(self, activity: bs.Activity, results: Any) -> None: 984 if self._locked: 985 bui.unlock_all_input() 986 987 # Any ending activity leads us into the main menu one. 988 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.
990 @override 991 def on_player_request(self, player: bs.SessionPlayer) -> bool: 992 # Reject all player requests. 993 return False
Called when a new bascenev1.Player wants to join the Session.
This should return True or False to accept/reject.