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