bascenev1lib.mainmenu

Session and Activity for displaying the main menu bg.

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

Wrangles news display.

NewsDisplay(activity: bascenev1._activity.Activity)
831    def __init__(self, activity: bs.Activity):
832        self._valid = True
833        self._message_duration = 10.0
834        self._message_spacing = 2.0
835        self._text: bs.NodeActor | None = None
836        self._activity = weakref.ref(activity)
837        self._phrases: list[str] = []
838        self._used_phrases: list[str] = []
839        self._phrase_change_timer: bs.Timer | None = None
840
841        # If we're signed in, fetch news immediately.
842        # Otherwise wait until we are signed in.
843        self._fetch_timer: bs.Timer | None = bs.Timer(
844            1.0, bs.WeakCall(self._try_fetching_news), repeat=True
845        )
846        self._try_fetching_news()