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