def __init__(self) -> None: # Gather dependencies we'll need (just our activity). self._activity_deps = ba.DependencySet(ba.Dependency(MainMenuActivity)) super().__init__([self._activity_deps]) self._locked = False self.setactivity(ba.newactivity(MainMenuActivity))
class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): """Activity showing the rotating main menu bg stuff.""" _stdassets = ba.Dependency(ba.AssetPackage, 'stdassets@1') def on_transition_in(self) -> None: super().on_transition_in() random.seed(123) self._logo_node: Optional[ba.Node] = None self._custom_logo_tex_name: Optional[str] = None self._word_actors: List[ba.Actor] = [] app = ba.app # FIXME: We shouldn't be doing things conditionally based on whether # the host is VR mode or not (clients may differ in that regard). # Any differences need to happen at the engine level so everyone # sees things in their own optimal way. vr_mode = ba.app.vr_mode if not ba.app.toolbar_test: color = ((1.0, 1.0, 1.0, 1.0) if vr_mode else (0.5, 0.6, 0.5, 0.6)) # FIXME: Need a node attr for vr-specific-scale. scale = (0.9 if (app.ui.uiscale is ba.UIScale.SMALL or vr_mode) else 0.7) self.my_name = ba.NodeActor( ba.newnode('text', attrs={ 'v_attach': 'bottom', 'h_align': 'center', 'color': color, 'flatness': 1.0, 'shadow': 1.0 if vr_mode else 0.5, 'scale': scale, 'position': (0, 10), 'vr_depth': -10, 'text': '\xa9 2011-2020 Eric Froemling' })) # Throw up some text that only clients can see so they know that the # host is navigating menus while they're just staring at an # empty-ish screen. tval = ba.Lstr(resource='hostIsNavigatingMenusText', subs=[('${HOST}', _ba.get_account_display_string())]) self._host_is_navigating_text = ba.NodeActor( ba.newnode('text', attrs={ 'text': tval, 'client_only': True, 'position': (0, -200), 'flatness': 1.0, 'h_align': 'center' })) if not ba.app.main_menu_did_initial_transition and hasattr( self, 'my_name'): assert self.my_name.node ba.animate(self.my_name.node, 'opacity', {2.3: 0, 3.0: 1.0}) # FIXME: We shouldn't be doing things conditionally based on whether # the host is vr mode or not (clients may not be or vice versa). # Any differences need to happen at the engine level so everyone sees # things in their own optimal way. vr_mode = app.vr_mode uiscale = app.ui.uiscale # In cases where we're doing lots of dev work lets always show the # build number. force_show_build_number = False if not ba.app.toolbar_test: if app.debug_build or app.test_build or force_show_build_number: if app.debug_build: text = ba.Lstr(value='${V} (${B}) (${D})', subs=[ ('${V}', app.version), ('${B}', str(app.build_number)), ('${D}', ba.Lstr(resource='debugText')), ]) else: text = ba.Lstr(value='${V} (${B})', subs=[ ('${V}', app.version), ('${B}', str(app.build_number)), ]) else: text = ba.Lstr(value='${V}', subs=[('${V}', app.version)]) scale = 0.9 if (uiscale is ba.UIScale.SMALL or vr_mode) else 0.7 color = (1, 1, 1, 1) if vr_mode else (0.5, 0.6, 0.5, 0.7) self.version = ba.NodeActor( ba.newnode( 'text', attrs={ 'v_attach': 'bottom', 'h_attach': 'right', 'h_align': 'right', 'flatness': 1.0, 'vr_depth': -10, 'shadow': 1.0 if vr_mode else 0.5, 'color': color, 'scale': scale, 'position': (-260, 10) if vr_mode else (-10, 10), 'text': text })) if not ba.app.main_menu_did_initial_transition: assert self.version.node ba.animate(self.version.node, 'opacity', {2.3: 0, 3.0: 1.0}) # Show the iircade logo on our iircade build. if app.iircade_mode: img = ba.NodeActor( ba.newnode('image', attrs={ 'texture': ba.gettexture('iircadeLogo'), 'attach': 'center', 'scale': (250, 250), 'position': (0, 0), 'tilt_translate': 0.21, 'absolute_scale': True })).autoretain() imgdelay = 0.0 if app.main_menu_did_initial_transition else 1.0 ba.animate(img.node, 'opacity', { imgdelay + 1.5: 0.0, imgdelay + 2.5: 1.0 }) # Throw in test build info. self.beta_info = self.beta_info_2 = None if app.test_build and not (app.demo_mode or app.arcade_mode): pos = ((230, 125) if (app.demo_mode or app.arcade_mode) else (230, 35)) self.beta_info = ba.NodeActor( ba.newnode('text', attrs={ 'v_attach': 'center', 'h_align': 'center', 'color': (1, 1, 1, 1), 'shadow': 0.5, 'flatness': 0.5, 'scale': 1, 'vr_depth': -60, 'position': pos, 'text': ba.Lstr(resource='testBuildText') })) if not ba.app.main_menu_did_initial_transition: assert self.beta_info.node ba.animate(self.beta_info.node, 'opacity', {1.3: 0, 1.8: 1.0}) model = ba.getmodel('thePadLevel') trees_model = ba.getmodel('trees') bottom_model = ba.getmodel('thePadLevelBottom') color_texture = ba.gettexture('thePadLevelColor') trees_texture = ba.gettexture('treesColor') bgtex = ba.gettexture('menuBG') bgmodel = ba.getmodel('thePadBG') # Load these last since most platforms don't use them. vr_bottom_fill_model = ba.getmodel('thePadVRFillBottom') vr_top_fill_model = ba.getmodel('thePadVRFillTop') gnode = self.globalsnode gnode.camera_mode = 'rotate' tint = (1.14, 1.1, 1.0) gnode.tint = tint gnode.ambient_color = (1.06, 1.04, 1.03) gnode.vignette_outer = (0.45, 0.55, 0.54) gnode.vignette_inner = (0.99, 0.98, 0.98) self.bottom = ba.NodeActor( ba.newnode('terrain', attrs={ 'model': bottom_model, 'lighting': False, 'reflection': 'soft', 'reflection_scale': [0.45], 'color_texture': color_texture })) self.vr_bottom_fill = ba.NodeActor( ba.newnode('terrain', attrs={ 'model': vr_bottom_fill_model, 'lighting': False, 'vr_only': True, 'color_texture': color_texture })) self.vr_top_fill = ba.NodeActor( ba.newnode('terrain', attrs={ 'model': vr_top_fill_model, 'vr_only': True, 'lighting': False, 'color_texture': bgtex })) self.terrain = ba.NodeActor( ba.newnode('terrain', attrs={ 'model': model, 'color_texture': color_texture, 'reflection': 'soft', 'reflection_scale': [0.3] })) self.trees = ba.NodeActor( ba.newnode('terrain', attrs={ 'model': trees_model, 'lighting': False, 'reflection': 'char', 'reflection_scale': [0.1], 'color_texture': trees_texture })) self.bgterrain = ba.NodeActor( ba.newnode('terrain', attrs={ 'model': bgmodel, 'color': (0.92, 0.91, 0.9), 'lighting': False, 'background': True, 'color_texture': bgtex })) self._ts = 0.86 self._language: Optional[str] = None self._update_timer = ba.Timer(1.0, self._update, repeat=True) self._update() # Hopefully this won't hitch but lets space these out anyway. _ba.add_clean_frame_callback(ba.WeakCall(self._start_preloads)) random.seed() # On the main menu, also show our news. class News: """Wrangles news display.""" def __init__(self, activity: ba.Activity): self._valid = True self._message_duration = 10.0 self._message_spacing = 2.0 self._text: Optional[ba.NodeActor] = None self._activity = weakref.ref(activity) # If we're signed in, fetch news immediately. # Otherwise wait until we are signed in. self._fetch_timer: Optional[ba.Timer] = ba.Timer( 1.0, ba.WeakCall(self._try_fetching_news), repeat=True) self._try_fetching_news() # We now want to wait until we're signed in before fetching news. def _try_fetching_news(self) -> None: if _ba.get_account_state() == 'signed_in': self._fetch_news() self._fetch_timer = None def _fetch_news(self) -> None: ba.app.main_menu_last_news_fetch_time = time.time() # UPDATE - We now just pull news from MRVs. news = _ba.get_account_misc_read_val('n', None) if news is not None: self._got_news(news) def _change_phrase(self) -> None: from bastd.actor.text import Text # If our news is way out of date, lets re-request it; # otherwise, rotate our phrase. assert ba.app.main_menu_last_news_fetch_time is not None if time.time() - ba.app.main_menu_last_news_fetch_time > 600.0: self._fetch_news() self._text = None else: if self._text is not None: if not self._phrases: for phr in self._used_phrases: self._phrases.insert(0, phr) val = self._phrases.pop() if val == '__ACH__': vrmode = app.vr_mode Text(ba.Lstr(resource='nextAchievementsText'), color=((1, 1, 1, 1) if vrmode else (0.95, 0.9, 1, 0.4)), host_only=True, maxwidth=200, position=(-300, -35), h_align=Text.HAlign.RIGHT, transition=Text.Transition.FADE_IN, scale=0.9 if vrmode else 0.7, flatness=1.0 if vrmode else 0.6, shadow=1.0 if vrmode else 0.5, h_attach=Text.HAttach.CENTER, v_attach=Text.VAttach.TOP, transition_delay=1.0, transition_out_delay=self._message_duration ).autoretain() achs = [ a for a in app.achievements if not a.complete ] if achs: ach = achs.pop( random.randrange(min(4, len(achs)))) ach.create_display( -180, -35, 1.0, outdelay=self._message_duration, style='news') if achs: ach = achs.pop( random.randrange(min(8, len(achs)))) ach.create_display( 180, -35, 1.25, outdelay=self._message_duration, style='news') else: spc = self._message_spacing keys = { spc: 0.0, spc + 1.0: 1.0, spc + self._message_duration - 1.0: 1.0, spc + self._message_duration: 0.0 } assert self._text.node ba.animate(self._text.node, 'opacity', keys) # {k: v # for k, v in list(keys.items())}) self._text.node.text = val def _got_news(self, news: str) -> None: # Run this stuff in the context of our activity since we # need to make nodes and stuff.. should fix the serverget # call so it. activity = self._activity() if activity is None or activity.expired: return with ba.Context(activity): self._phrases: List[str] = [] # Show upcoming achievements in non-vr versions # (currently too hard to read in vr). self._used_phrases = ( ['__ACH__'] if not ba.app.vr_mode else []) + [s for s in news.split('<br>\n') if s != ''] self._phrase_change_timer = ba.Timer( (self._message_duration + self._message_spacing), ba.WeakCall(self._change_phrase), repeat=True) scl = 1.2 if (ba.app.ui.uiscale is ba.UIScale.SMALL or ba.app.vr_mode) else 0.8 color2 = ((1, 1, 1, 1) if ba.app.vr_mode else (0.7, 0.65, 0.75, 1.0)) shadow = (1.0 if ba.app.vr_mode else 0.4) self._text = ba.NodeActor( ba.newnode('text', attrs={ 'v_attach': 'top', 'h_attach': 'center', 'h_align': 'center', 'vr_depth': -20, 'shadow': shadow, 'flatness': 0.8, 'v_align': 'top', 'color': color2, 'scale': scl, 'maxwidth': 900.0 / scl, 'position': (0, -10) })) self._change_phrase() if not (app.demo_mode or app.arcade_mode) and not app.toolbar_test: self._news = News(self) # Bring up the last place we were, or start at the main menu otherwise. with ba.Context('ui'): from bastd.ui import specialoffer if bool(False): uicontroller = ba.app.ui.controller assert uicontroller is not None uicontroller.show_main_menu() else: main_menu_location = ba.app.ui.get_main_menu_location() # When coming back from a kiosk-mode game, jump to # the kiosk start screen. if ba.app.demo_mode or ba.app.arcade_mode: # pylint: disable=cyclic-import from bastd.ui.kiosk import KioskWindow ba.app.ui.set_main_menu_window( KioskWindow().get_root_widget()) # ..or in normal cases go back to the main menu else: if main_menu_location == 'Gather': # pylint: disable=cyclic-import from bastd.ui.gather import GatherWindow ba.app.ui.set_main_menu_window( GatherWindow(transition=None).get_root_widget()) elif main_menu_location == 'Watch': # pylint: disable=cyclic-import from bastd.ui.watch import WatchWindow ba.app.ui.set_main_menu_window( WatchWindow(transition=None).get_root_widget()) elif main_menu_location == 'Team Game Select': # pylint: disable=cyclic-import from bastd.ui.playlist.browser import ( PlaylistBrowserWindow) ba.app.ui.set_main_menu_window( PlaylistBrowserWindow( sessiontype=ba.DualTeamSession, transition=None).get_root_widget()) elif main_menu_location == 'Free-for-All Game Select': # pylint: disable=cyclic-import from bastd.ui.playlist.browser import ( PlaylistBrowserWindow) ba.app.ui.set_main_menu_window( PlaylistBrowserWindow( sessiontype=ba.FreeForAllSession, transition=None).get_root_widget()) elif main_menu_location == 'Coop Select': # pylint: disable=cyclic-import from bastd.ui.coop.browser import CoopBrowserWindow ba.app.ui.set_main_menu_window( CoopBrowserWindow( transition=None).get_root_widget()) else: # pylint: disable=cyclic-import from bastd.ui.mainmenu import MainMenuWindow ba.app.ui.set_main_menu_window( MainMenuWindow(transition=None).get_root_widget()) # attempt to show any pending offers immediately. # If that doesn't work, try again in a few seconds # (we may not have heard back from the server) # ..if that doesn't work they'll just have to wait # until the next opportunity. if not specialoffer.show_offer(): def try_again() -> None: if not specialoffer.show_offer(): # Try one last time.. ba.timer(2.0, specialoffer.show_offer, timetype=ba.TimeType.REAL) ba.timer(2.0, try_again, timetype=ba.TimeType.REAL) ba.app.main_menu_did_initial_transition = True def _update(self) -> None: app = ba.app # Update logo in case it changes. if self._logo_node: custom_texture = self._get_custom_logo_tex_name() if custom_texture != self._custom_logo_tex_name: self._custom_logo_tex_name = custom_texture self._logo_node.texture = ba.gettexture( custom_texture if custom_texture is not None else 'logo') self._logo_node.model_opaque = (None if custom_texture is not None else ba.getmodel('logo')) self._logo_node.model_transparent = ( None if custom_texture is not None else ba.getmodel('logoTransparent')) # If language has changed, recreate our logo text/graphics. lang = app.language if lang != self._language: self._language = lang y = 20 base_scale = 1.1 self._word_actors = [] base_delay = 1.0 delay = base_delay delay_inc = 0.02 # Come on faster after the first time. if app.main_menu_did_initial_transition: base_delay = 0.0 delay = base_delay delay_inc = 0.02 # We draw higher in kiosk mode (make sure to test this # when making adjustments) for now we're hard-coded for # a few languages.. should maybe look into generalizing this?.. if app.language == 'Chinese': base_x = -270.0 x = base_x - 20.0 spacing = 85.0 * base_scale y_extra = 0.0 if (app.demo_mode or app.arcade_mode) else 0.0 self._make_logo(x - 110 + 50, 113 + y + 1.2 * y_extra, 0.34 * base_scale, delay=base_delay + 0.1, custom_texture='chTitleChar1', jitter_scale=2.0, vr_depth_offset=-30) x += spacing delay += delay_inc self._make_logo(x - 10 + 50, 110 + y + 1.2 * y_extra, 0.31 * base_scale, delay=base_delay + 0.15, custom_texture='chTitleChar2', jitter_scale=2.0, vr_depth_offset=-30) x += 2.0 * spacing delay += delay_inc self._make_logo(x + 180 - 140, 110 + y + 1.2 * y_extra, 0.3 * base_scale, delay=base_delay + 0.25, custom_texture='chTitleChar3', jitter_scale=2.0, vr_depth_offset=-30) x += spacing delay += delay_inc self._make_logo(x + 241 - 120, 110 + y + 1.2 * y_extra, 0.31 * base_scale, delay=base_delay + 0.3, custom_texture='chTitleChar4', jitter_scale=2.0, vr_depth_offset=-30) x += spacing delay += delay_inc self._make_logo(x + 300 - 90, 105 + y + 1.2 * y_extra, 0.34 * base_scale, delay=base_delay + 0.35, custom_texture='chTitleChar5', jitter_scale=2.0, vr_depth_offset=-30) self._make_logo(base_x + 155, 146 + y + 1.2 * y_extra, 0.28 * base_scale, delay=base_delay + 0.2, rotate=-7) else: base_x = -170 x = base_x - 20 spacing = 55 * base_scale y_extra = 0 if (app.demo_mode or app.arcade_mode) else 0 xv1 = x delay1 = delay for shadow in (True, False): x = xv1 delay = delay1 self._make_word('B', x - 50, y - 23 + 0.8 * y_extra, scale=1.3 * base_scale, delay=delay, vr_depth_offset=3, shadow=shadow) x += spacing delay += delay_inc self._make_word('m', x, y + y_extra, delay=delay, scale=base_scale, shadow=shadow) x += spacing * 1.25 delay += delay_inc self._make_word('b', x, y + y_extra - 10, delay=delay, scale=1.1 * base_scale, vr_depth_offset=5, shadow=shadow) x += spacing * 0.85 delay += delay_inc self._make_word('S', x, y - 25 + 0.8 * y_extra, scale=1.35 * base_scale, delay=delay, vr_depth_offset=14, shadow=shadow) x += spacing delay += delay_inc self._make_word('q', x, y + y_extra, delay=delay, scale=base_scale, shadow=shadow) x += spacing * 0.9 delay += delay_inc self._make_word('u', x, y + y_extra, delay=delay, scale=base_scale, vr_depth_offset=7, shadow=shadow) x += spacing * 0.9 delay += delay_inc self._make_word('a', x, y + y_extra, delay=delay, scale=base_scale, shadow=shadow) x += spacing * 0.64 delay += delay_inc self._make_word('d', x, y + y_extra - 10, delay=delay, scale=1.1 * base_scale, vr_depth_offset=6, shadow=shadow) self._make_logo(base_x - 28, 125 + y + 1.2 * y_extra, 0.32 * base_scale, delay=base_delay) def _make_word(self, word: str, x: float, y: float, scale: float = 1.0, delay: float = 0.0, vr_depth_offset: float = 0.0, shadow: bool = False) -> None: if shadow: word_obj = ba.NodeActor( ba.newnode('text', attrs={ 'position': (x, y), 'big': True, 'color': (0.0, 0.0, 0.2, 0.08), 'tilt_translate': 0.09, 'opacity_scales_shadow': False, 'shadow': 0.2, 'vr_depth': -130, 'v_align': 'center', 'project_scale': 0.97 * scale, 'scale': 1.0, 'text': word })) self._word_actors.append(word_obj) else: word_obj = ba.NodeActor( ba.newnode('text', attrs={ 'position': (x, y), 'big': True, 'color': (1.2, 1.15, 1.15, 1.0), 'tilt_translate': 0.11, 'shadow': 0.2, 'vr_depth': -40 + vr_depth_offset, 'v_align': 'center', 'project_scale': scale, 'scale': 1.0, 'text': word })) self._word_actors.append(word_obj) # Add a bit of stop-motion-y jitter to the logo # (unless we're in VR mode in which case its best to # leave things still). if not ba.app.vr_mode: cmb: Optional[ba.Node] cmb2: Optional[ba.Node] if not shadow: cmb = ba.newnode('combine', owner=word_obj.node, attrs={'size': 2}) else: cmb = None if shadow: cmb2 = ba.newnode('combine', owner=word_obj.node, attrs={'size': 2}) else: cmb2 = None if not shadow: assert cmb and word_obj.node cmb.connectattr('output', word_obj.node, 'position') if shadow: assert cmb2 and word_obj.node cmb2.connectattr('output', word_obj.node, 'position') keys = {} keys2 = {} time_v = 0.0 for _i in range(10): val = x + (random.random() - 0.5) * 0.8 val2 = x + (random.random() - 0.5) * 0.8 keys[time_v * self._ts] = val keys2[time_v * self._ts] = val2 + 5 time_v += random.random() * 0.1 if cmb is not None: ba.animate(cmb, 'input0', keys, loop=True) if cmb2 is not None: ba.animate(cmb2, 'input0', keys2, loop=True) keys = {} keys2 = {} time_v = 0 for _i in range(10): val = y + (random.random() - 0.5) * 0.8 val2 = y + (random.random() - 0.5) * 0.8 keys[time_v * self._ts] = val keys2[time_v * self._ts] = val2 - 9 time_v += random.random() * 0.1 if cmb is not None: ba.animate(cmb, 'input1', keys, loop=True) if cmb2 is not None: ba.animate(cmb2, 'input1', keys2, loop=True) if not shadow: assert word_obj.node ba.animate(word_obj.node, 'project_scale', { delay: 0.0, delay + 0.1: scale * 1.1, delay + 0.2: scale }) else: assert word_obj.node ba.animate(word_obj.node, 'project_scale', { delay: 0.0, delay + 0.1: scale * 1.1, delay + 0.2: scale }) def _get_custom_logo_tex_name(self) -> Optional[str]: if _ba.get_account_misc_read_val('easter', False): return 'logoEaster' return None # Pop the logo and menu in. def _make_logo(self, x: float, y: float, scale: float, delay: float, custom_texture: str = None, jitter_scale: float = 1.0, rotate: float = 0.0, vr_depth_offset: float = 0.0) -> None: # Temp easter goodness. if custom_texture is None: custom_texture = self._get_custom_logo_tex_name() self._custom_logo_tex_name = custom_texture ltex = ba.gettexture( custom_texture if custom_texture is not None else 'logo') mopaque = (None if custom_texture is not None else ba.getmodel('logo')) mtrans = (None if custom_texture is not None else ba.getmodel('logoTransparent')) logo = ba.NodeActor( ba.newnode('image', attrs={ 'texture': ltex, 'model_opaque': mopaque, 'model_transparent': mtrans, 'vr_depth': -10 + vr_depth_offset, 'rotate': rotate, 'attach': 'center', 'tilt_translate': 0.21, 'absolute_scale': True })) self._logo_node = logo.node self._word_actors.append(logo) # Add a bit of stop-motion-y jitter to the logo # (unless we're in VR mode in which case its best to # leave things still). assert logo.node if not ba.app.vr_mode: cmb = ba.newnode('combine', owner=logo.node, attrs={'size': 2}) cmb.connectattr('output', logo.node, 'position') keys = {} time_v = 0.0 # Gen some random keys for that stop-motion-y look for _i in range(10): keys[time_v] = x + (random.random() - 0.5) * 0.7 * jitter_scale time_v += random.random() * 0.1 ba.animate(cmb, 'input0', keys, loop=True) keys = {} time_v = 0.0 for _i in range(10): keys[time_v * self._ts] = y + (random.random() - 0.5) * 0.7 * jitter_scale time_v += random.random() * 0.1 ba.animate(cmb, 'input1', keys, loop=True) else: logo.node.position = (x, y) cmb = ba.newnode('combine', owner=logo.node, attrs={'size': 2}) keys = { delay: 0.0, delay + 0.1: 700.0 * scale, delay + 0.2: 600.0 * scale } ba.animate(cmb, 'input0', keys) ba.animate(cmb, 'input1', keys) cmb.connectattr('output', logo.node, 'scale') def _start_preloads(self) -> None: # FIXME: The func that calls us back doesn't save/restore state # or check for a dead activity so we have to do that ourself. if self.expired: return with ba.Context(self): _preload1() ba.timer(0.5, lambda: ba.setmusic(ba.MusicType.MENU))