bastd.mainmenu

Session and Activity for displaying the main menu bg.

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