bascenev1lib.mainmenu

Session and Activity for displaying the main menu bg.

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

Wrangles news display.

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