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