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