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