bascenev1lib.mainmenu

Session and Activity for displaying the main menu bg.

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

Wrangles news display.

NewsDisplay(activity: bascenev1._activity.Activity)
871    def __init__(self, activity: bs.Activity):
872        self._valid = True
873        self._message_duration = 10.0
874        self._message_spacing = 2.0
875        self._text: bs.NodeActor | None = None
876        self._activity = weakref.ref(activity)
877        self._phrases: list[str] = []
878        self._used_phrases: list[str] = []
879        self._phrase_change_timer: bs.Timer | None = None
880
881        # If we're signed in, fetch news immediately.
882        # Otherwise wait until we are signed in.
883        self._fetch_timer: bs.Timer | None = bs.Timer(
884            1.0, bs.WeakCall(self._try_fetching_news), repeat=True
885        )
886        self._try_fetching_news()