def on_begin(self) -> None: from ba._analytics import game_begin_analytics super().on_begin() game_begin_analytics() # We don't do this in on_transition_in because it may depend on # players/teams which aren't available until now. _ba.timer(0.001, self._show_scoreboard_info) _ba.timer(1.0, self._show_info) _ba.timer(2.5, self._show_tip) # Store some basic info about players present at start time. self.initialplayerinfos = [ PlayerInfo(name=p.getname(full=True), character=p.character) for p in self.players ] # Sort this by name so high score lists/etc will be consistent # regardless of player join order. self.initialplayerinfos.sort(key=lambda x: x.name) # If this is a tournament, query info about it such as how much # time is left. tournament_id = self.session.tournament_id if tournament_id is not None: _ba.tournament_query( args={ 'tournamentIDs': [tournament_id], 'source': 'in-game time remaining query' }, callback=WeakCall(self._on_tournament_query_response), )
def _standard_drop_powerups(self) -> None: """Standard powerup drop.""" # Drop one powerup per point. points = self.map.powerup_spawn_points for i in range(len(points)): _ba.timer(i * 0.4, WeakCall(self._standard_drop_powerup, i))
def continue_or_end_game(self) -> None: """If continues are allowed, prompts the player to purchase a continue and calls either end_game or continue_game depending on the result""" # pylint: disable=too-many-nested-blocks # pylint: disable=cyclic-import from bastd.ui.continues import ContinuesWindow from ba._coopsession import CoopSession from ba._enums import TimeType try: if _ba.get_account_misc_read_val('enableContinues', False): session = self.session # We only support continuing in non-tournament games. tournament_id = session.tournament_id if tournament_id is None: # We currently only support continuing in sequential # co-op campaigns. if isinstance(session, CoopSession): assert session.campaign is not None if session.campaign.sequential: gnode = self.globalsnode # Only attempt this if we're not currently paused # and there appears to be no UI. if (not gnode.paused and not _ba.app.ui.has_main_menu_window()): self._is_waiting_for_continue = True with _ba.Context('ui'): _ba.timer( 0.5, lambda: ContinuesWindow( self, self._continue_cost, continue_call=WeakCall( self._continue_choice, True), cancel_call=WeakCall( self._continue_choice, False)), timetype=TimeType.REAL) return except Exception: print_exception('Error handling continues.') self.end_game()
def on_player_join(self, player: EmptyPlayer) -> None: from ba._general import WeakCall super().on_player_join(player) time_till_assign = max( 0, self._birth_time + self._min_view_time - _ba.time()) # If we're still kicking at the end of our assign-delay, assign this # guy's input to trigger us. _ba.timer(time_till_assign, WeakCall(self._safe_assign, player))
def __init__(self, lobby: ba.Lobby): from ba._nodeactor import NodeActor from ba._general import WeakCall self._state = 0 self._press_to_punch: Union[str, ba.Lstr] = ('C' if _ba.app.iircade_mode else _ba.charstr( SpecialChar.LEFT_BUTTON)) self._press_to_bomb: Union[str, ba.Lstr] = ('B' if _ba.app.iircade_mode else _ba.charstr( SpecialChar.RIGHT_BUTTON)) self._joinmsg = Lstr(resource='pressAnyButtonToJoinText') can_switch_teams = (len(lobby.sessionteams) > 1) # If we have a keyboard, grab keys for punch and pickup. # FIXME: This of course is only correct on the local device; # Should change this for net games. keyboard = _ba.getinputdevice('Keyboard', '#1', doraise=False) if keyboard is not None: self._update_for_keyboard(keyboard) flatness = 1.0 if _ba.app.vr_mode else 0.0 self._text = NodeActor( _ba.newnode('text', attrs={ 'position': (0, -40), 'h_attach': 'center', 'v_attach': 'top', 'h_align': 'center', 'color': (0.7, 0.7, 0.95, 1.0), 'flatness': flatness, 'text': self._joinmsg })) if _ba.app.demo_mode or _ba.app.arcade_mode: self._messages = [self._joinmsg] else: msg1 = Lstr(resource='pressToSelectProfileText', subs=[ ('${BUTTONS}', _ba.charstr(SpecialChar.UP_ARROW) + ' ' + _ba.charstr(SpecialChar.DOWN_ARROW)) ]) msg2 = Lstr(resource='pressToOverrideCharacterText', subs=[('${BUTTONS}', Lstr(resource='bombBoldText'))]) msg3 = Lstr(value='${A} < ${B} >', subs=[('${A}', msg2), ('${B}', self._press_to_bomb)]) self._messages = (([ Lstr( resource='pressToSelectTeamText', subs=[('${BUTTONS}', _ba.charstr(SpecialChar.LEFT_ARROW) + ' ' + _ba.charstr(SpecialChar.RIGHT_ARROW))], ) ] if can_switch_teams else []) + [msg1] + [msg3] + [self._joinmsg]) self._timer = _ba.Timer(4.0, WeakCall(self._update), repeat=True)
def on_begin(self) -> None: super().on_begin() # Show achievements remaining. if not _ba.app.kiosk_mode: _ba.timer(3.8, WeakCall(self._show_remaining_achievements)) # Preload achievement images in case we get some. _ba.timer(2.0, WeakCall(self._preload_achievements)) # Let's ask the server for a 'time-to-beat' value. levelname = self._get_coop_level_name() campaign = self.session.campaign assert campaign is not None config_str = (str(len(self.players)) + 'p' + campaign.getlevel( self.settings_raw['name']).get_score_version_string().replace( ' ', '_')) _ba.get_scores_to_beat(levelname, config_str, WeakCall(self._on_got_scores_to_beat))
def transition_in(self, prev_globals: Optional[ba.Node]) -> None: """Called by Session to kick off transition-in. (internal) """ from ba._general import WeakCall from ba._gameutils import sharedobj assert not self._has_transitioned_in self._has_transitioned_in = True # Set up the globals node based on our settings. with _ba.Context(self): # Now that it's going to be front and center, # set some global values based on what the activity wants. glb = sharedobj('globals') glb.use_fixed_vr_overlay = self.use_fixed_vr_overlay glb.allow_kick_idle_players = self.allow_kick_idle_players if self.inherits_slow_motion and prev_globals is not None: glb.slow_motion = prev_globals.slow_motion else: glb.slow_motion = self.slow_motion if self.inherits_music and prev_globals is not None: glb.music_continuous = True # Prevent restarting same music. glb.music = prev_globals.music glb.music_count += 1 if self.inherits_camera_vr_offset and prev_globals is not None: glb.vr_camera_offset = prev_globals.vr_camera_offset if self.inherits_vr_overlay_center and prev_globals is not None: glb.vr_overlay_center = prev_globals.vr_overlay_center glb.vr_overlay_center_enabled = ( prev_globals.vr_overlay_center_enabled) # If they want to inherit tint from the previous self. if self.inherits_tint and prev_globals is not None: glb.tint = prev_globals.tint glb.vignette_outer = prev_globals.vignette_outer glb.vignette_inner = prev_globals.vignette_inner # Start pruning our transient actors periodically. self._prune_dead_actors_timer = _ba.Timer( 5.17, WeakCall(self._prune_dead_actors), repeat=True) self._prune_dead_actors() # Also start our low-level scene running. self._activity_data.start() try: self.on_transition_in() except Exception: print_exception('Error in on_transition_in for', self) # Tell the C++ layer that this activity is the main one, so it uses # settings from our globals, directs various events to us, etc. self._activity_data.make_foreground()
def setup_standard_powerup_drops(self, enable_tnt: bool = True) -> None: """Create standard powerup drops for the current map.""" # pylint: disable=cyclic-import from bastd.actor.powerupbox import DEFAULT_POWERUP_INTERVAL self._powerup_drop_timer = _ba.Timer(DEFAULT_POWERUP_INTERVAL, WeakCall( self._standard_drop_powerups), repeat=True) self._standard_drop_powerups() if enable_tnt: self._tnt_spawners = {} self._setup_standard_tnt_drops()
def on_transition_in(self) -> None: """Called when the Activity is first becoming visible. Upon this call, the Activity should fade in backgrounds, start playing music, etc. It does not yet have access to ba.Players or ba.Teams, however. They remain owned by the previous Activity up until ba.Activity.on_begin() is called. """ from ba._general import WeakCall self._called_activity_on_transition_in = True # Start pruning our transient actors periodically. self._prune_dead_objects_timer = _ba.Timer( 5.17, WeakCall(self._prune_dead_objects), repeat=True) self._prune_dead_objects() # Also start our low-level scene-graph running. self._activity_data.start()
def respawn_player(self, player: PlayerType, respawn_time: Optional[float] = None) -> None: """ Given a ba.Player, sets up a standard respawn timer, along with the standard counter display, etc. At the end of the respawn period spawn_player() will be called if the Player still exists. An explicit 'respawn_time' can optionally be provided (in seconds). """ # pylint: disable=cyclic-import assert player if respawn_time is None: teamsize = len(player.team.players) if teamsize == 1: respawn_time = 3.0 elif teamsize == 2: respawn_time = 5.0 elif teamsize == 3: respawn_time = 6.0 else: respawn_time = 7.0 # If this standard setting is present, factor it in. if 'Respawn Times' in self.settings_raw: respawn_time *= self.settings_raw['Respawn Times'] # We want whole seconds. assert respawn_time is not None respawn_time = round(max(1.0, respawn_time), 0) if player.actor and not self.has_ended(): from bastd.actor.respawnicon import RespawnIcon player.customdata['respawn_timer'] = _ba.Timer( respawn_time, WeakCall(self.spawn_player_if_exists, player)) player.customdata['respawn_icon'] = RespawnIcon( player, respawn_time)
def setup_standard_time_limit(self, duration: float) -> None: """ Create a standard game time-limit given the provided duration in seconds. This will be displayed at the top of the screen. If the time-limit expires, end_game() will be called. """ from ba._nodeactor import NodeActor if duration <= 0.0: return self._standard_time_limit_time = int(duration) self._standard_time_limit_timer = _ba.Timer( 1.0, WeakCall(self._standard_time_limit_tick), repeat=True) self._standard_time_limit_text = NodeActor( _ba.newnode('text', attrs={ 'v_attach': 'top', 'h_attach': 'center', 'h_align': 'left', 'color': (1.0, 1.0, 1.0, 0.5), 'position': (-25, -30), 'flatness': 1.0, 'scale': 0.9 })) self._standard_time_limit_text_input = NodeActor( _ba.newnode('timedisplay', attrs={ 'time2': duration * 1000, 'timemin': 0 })) self.globalsnode.connectattr('time', self._standard_time_limit_text_input.node, 'time1') assert self._standard_time_limit_text_input.node assert self._standard_time_limit_text.node self._standard_time_limit_text_input.node.connectattr( 'output', self._standard_time_limit_text.node, 'text')
def on_activity_end(self, activity: ba.Activity, results: Any) -> None: """Method override for co-op sessions. Jumps between co-op games and score screens. """ # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-statements # pylint: disable=cyclic-import from ba._activitytypes import JoiningActivity, TransitionActivity from ba._lang import Lstr from ba._general import WeakCall from ba._coopgame import CoopGameActivity from ba._gameresults import TeamGameResults from bastd.tutorial import TutorialActivity from bastd.activity.coopscorescreen import CoopScoreScreen app = _ba.app # If we're running a TeamGameActivity we'll have a TeamGameResults # as results. Otherwise its an old CoopGameActivity so its giving # us a dict of random stuff. if isinstance(results, TeamGameResults): outcome = 'defeat' # This can't be 'beaten'. else: try: outcome = results['outcome'] except Exception: outcome = '' # If at any point we have no in-game players, quit out of the session # (this can happen if someone leaves in the tutorial for instance). active_players = [p for p in self.players if p.in_game] if not active_players: self.end() return # If we're in a between-round activity or a restart-activity, # hop into a round. if (isinstance( activity, (JoiningActivity, CoopScoreScreen, TransitionActivity))): if outcome == 'next_level': if self._next_game_instance is None: raise Exception() assert self._next_game_name is not None self.campaign_state['level'] = self._next_game_name next_game = self._next_game_instance else: next_game = self._current_game_instance # Special case: if we're coming from a joining-activity # and will be going into onslaught-training, show the # tutorial first. if (isinstance(activity, JoiningActivity) and self.campaign_state['level'] == 'Onslaught Training' and not app.kiosk_mode): if self._tutorial_activity is None: raise Exception("tutorial not preloaded properly") self.set_activity(self._tutorial_activity) self._tutorial_activity = None self._ran_tutorial_activity = True self._custom_menu_ui = [] # Normal case; launch the next round. else: # Reset stats for the new activity. self.stats.reset() for player in self.players: # Skip players that are still choosing a team. if player.in_game: self.stats.register_player(player) self.stats.set_activity(next_game) # Now flip the current activity. self.set_activity(next_game) if not app.kiosk_mode: if self.tournament_id is not None: self._custom_menu_ui = [{ 'label': Lstr(resource='restartText'), 'resume_on_call': False, 'call': WeakCall(self._on_tournament_restart_menu_press) }] else: self._custom_menu_ui = [{ 'label': Lstr(resource='restartText'), 'call': WeakCall(self.restart) }] # If we were in a tutorial, just pop a transition to get to the # actual round. elif isinstance(activity, TutorialActivity): self.set_activity(_ba.new_activity(TransitionActivity)) else: # Generic team games. if isinstance(results, TeamGameResults): player_info = results.get_player_info() score = results.get_team_score(results.get_teams()[0]) fail_message = None score_order = ('decreasing' if results.get_lower_is_better() else 'increasing') if results.get_score_type() in ('seconds', 'milliseconds', 'time'): score_type = 'time' # Results contains milliseconds; ScoreScreen wants # hundredths; need to fix :-/ if score is not None: score //= 10 else: if results.get_score_type() != 'points': print(("Unknown score type: '" + results.get_score_type() + "'")) score_type = 'points' # Old coop-game-specific results; should migrate away from these. else: player_info = (results['player_info'] if 'player_info' in results else None) score = results['score'] if 'score' in results else None fail_message = (results['fail_message'] if 'fail_message' in results else None) score_order = (results['score_order'] if 'score_order' in results else 'increasing') activity_score_type = (activity.get_score_type() if isinstance( activity, CoopGameActivity) else None) assert activity_score_type is not None score_type = activity_score_type # Looks like we were in a round - check the outcome and # go from there. if outcome == 'restart': # This will pop up back in the same round. self.set_activity(_ba.new_activity(TransitionActivity)) else: self.set_activity( _ba.new_activity( CoopScoreScreen, { 'player_info': player_info, 'score': score, 'fail_message': fail_message, 'score_order': score_order, 'score_type': score_type, 'outcome': outcome, 'campaign': self.campaign, 'level': self.campaign_state['level'] })) # No matter what, get the next 2 levels ready to go. self._update_on_deck_game_instances()
def on_player_leave(self, player: ba.Player) -> None: from ba._general import WeakCall super().on_player_leave(player) # If all our players leave we wanna quit out of the session. _ba.timer(2.0, WeakCall(self._end_session_if_empty))
def __init__(self, lobby: ba.Lobby): # pylint: disable=too-many-locals from ba import _input from ba._lang import Lstr from ba._nodeactor import NodeActor from ba._general import WeakCall from ba._enums import SpecialChar can_switch_teams = (len(lobby.teams) > 1) self._state = 0 press_to_punch: Union[str, ba.Lstr] = _ba.charstr(SpecialChar.LEFT_BUTTON) press_to_bomb: Union[str, ba.Lstr] = _ba.charstr(SpecialChar.RIGHT_BUTTON) # If we have a keyboard, grab keys for punch and pickup. # FIXME: This of course is only correct on the local device; # Should change this for net games. keyboard: Optional[ba.InputDevice] = _ba.get_input_device( 'Keyboard', '#1', doraise=False) if keyboard is not None: punch_key = keyboard.get_button_name( _input.get_device_value(keyboard, 'buttonPunch')) press_to_punch = Lstr(resource='orText', subs=[('${A}', Lstr(value='\'${K}\'', subs=[('${K}', punch_key)])), ('${B}', press_to_punch)]) bomb_key = keyboard.get_button_name( _input.get_device_value(keyboard, 'buttonBomb')) press_to_bomb = Lstr(resource='orText', subs=[('${A}', Lstr(value='\'${K}\'', subs=[('${K}', bomb_key)])), ('${B}', press_to_bomb)]) join_str = Lstr(value='${A} < ${B} >', subs=[('${A}', Lstr(resource='pressPunchToJoinText')), ('${B}', press_to_punch)]) else: join_str = Lstr(resource='pressAnyButtonToJoinText') flatness = 1.0 if _ba.app.vr_mode else 0.0 self._text = NodeActor( _ba.newnode('text', attrs={ 'position': (0, -40), 'h_attach': 'center', 'v_attach': 'top', 'h_align': 'center', 'color': (0.7, 0.7, 0.95, 1.0), 'flatness': flatness, 'text': join_str })) if _ba.app.kiosk_mode: self._messages = [join_str] else: msg1 = Lstr(resource='pressToSelectProfileText', subs=[ ('${BUTTONS}', _ba.charstr(SpecialChar.UP_ARROW) + ' ' + _ba.charstr(SpecialChar.DOWN_ARROW)) ]) msg2 = Lstr(resource='pressToOverrideCharacterText', subs=[('${BUTTONS}', Lstr(resource='bombBoldText'))]) msg3 = Lstr(value='${A} < ${B} >', subs=[('${A}', msg2), ('${B}', press_to_bomb)]) self._messages = (([ Lstr(resource='pressToSelectTeamText', subs=[('${BUTTONS}', _ba.charstr(SpecialChar.LEFT_ARROW) + ' ' + _ba.charstr(SpecialChar.RIGHT_ARROW))]) ] if can_switch_teams else []) + [msg1] + [msg3] + [join_str]) self._timer = _ba.Timer(4.0, WeakCall(self._update), repeat=True)
def on_player_leave(self, sessionplayer: ba.SessionPlayer) -> None: from ba._general import WeakCall super().on_player_leave(sessionplayer) _ba.timer(2.0, WeakCall(self._handle_empty_activity))
def _setup_tournament_time_limit(self, duration: float) -> None: """ Create a tournament game time-limit given the provided duration in seconds. This will be displayed at the top of the screen. If the time-limit expires, end_game() will be called. """ from ba._nodeactor import NodeActor from ba._enums import TimeType if duration <= 0.0: return self._tournament_time_limit = int(duration) # We want this timer to match the server's time as close as possible, # so lets go with base-time. Theoretically we should do real-time but # then we have to mess with contexts and whatnot since its currently # not available in activity contexts. :-/ self._tournament_time_limit_timer = _ba.Timer( 1.0, WeakCall(self._tournament_time_limit_tick), repeat=True, timetype=TimeType.BASE) self._tournament_time_limit_title_text = NodeActor( _ba.newnode('text', attrs={ 'v_attach': 'bottom', 'h_attach': 'left', 'h_align': 'center', 'v_align': 'center', 'vr_depth': 300, 'maxwidth': 100, 'color': (1.0, 1.0, 1.0, 0.5), 'position': (60, 50), 'flatness': 1.0, 'scale': 0.5, 'text': Lstr(resource='tournamentText') })) self._tournament_time_limit_text = NodeActor( _ba.newnode('text', attrs={ 'v_attach': 'bottom', 'h_attach': 'left', 'h_align': 'center', 'v_align': 'center', 'vr_depth': 300, 'maxwidth': 100, 'color': (1.0, 1.0, 1.0, 0.5), 'position': (60, 30), 'flatness': 1.0, 'scale': 0.9 })) self._tournament_time_limit_text_input = NodeActor( _ba.newnode('timedisplay', attrs={ 'timemin': 0, 'time2': self._tournament_time_limit * 1000 })) assert self._tournament_time_limit_text.node assert self._tournament_time_limit_text_input.node self._tournament_time_limit_text_input.node.connectattr( 'output', self._tournament_time_limit_text.node, 'text')
def on_activity_end(self, activity: ba.Activity, results: Any) -> None: """Method override for co-op sessions. Jumps between co-op games and score screens. """ # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-statements # pylint: disable=cyclic-import from ba._activitytypes import JoinActivity, TransitionActivity from ba._language import Lstr from ba._general import WeakCall from ba._coopgame import CoopGameActivity from ba._gameresults import GameResults from ba._score import ScoreType from ba._player import PlayerInfo from bastd.tutorial import TutorialActivity from bastd.activity.coopscore import CoopScoreScreen app = _ba.app # If we're running a TeamGameActivity we'll have a GameResults # as results. Otherwise its an old CoopGameActivity so its giving # us a dict of random stuff. if isinstance(results, GameResults): outcome = 'defeat' # This can't be 'beaten'. else: outcome = '' if results is None else results.get('outcome', '') # If we're running with a gui and at any point we have no # in-game players, quit out of the session (this can happen if # someone leaves in the tutorial for instance). if not _ba.app.headless_mode: active_players = [p for p in self.sessionplayers if p.in_game] if not active_players: self.end() return # If we're in a between-round activity or a restart-activity, # hop into a round. if (isinstance(activity, (JoinActivity, CoopScoreScreen, TransitionActivity))): if outcome == 'next_level': if self._next_game_instance is None: raise RuntimeError() assert self._next_game_level_name is not None self.campaign_level_name = self._next_game_level_name next_game = self._next_game_instance else: next_game = self._current_game_instance # Special case: if we're coming from a joining-activity # and will be going into onslaught-training, show the # tutorial first. if (isinstance(activity, JoinActivity) and self.campaign_level_name == 'Onslaught Training' and not (app.demo_mode or app.arcade_mode)): if self._tutorial_activity is None: raise RuntimeError('Tutorial not preloaded properly.') self.setactivity(self._tutorial_activity) self._tutorial_activity = None self._ran_tutorial_activity = True self._custom_menu_ui = [] # Normal case; launch the next round. else: # Reset stats for the new activity. self.stats.reset() for player in self.sessionplayers: # Skip players that are still choosing a team. if player.in_game: self.stats.register_sessionplayer(player) self.stats.setactivity(next_game) # Now flip the current activity.. self.setactivity(next_game) if not (app.demo_mode or app.arcade_mode): if self.tournament_id is not None: self._custom_menu_ui = [{ 'label': Lstr(resource='restartText'), 'resume_on_call': False, 'call': WeakCall(self._on_tournament_restart_menu_press) }] else: self._custom_menu_ui = [{ 'label': Lstr(resource='restartText'), 'call': WeakCall(self.restart) }] # If we were in a tutorial, just pop a transition to get to the # actual round. elif isinstance(activity, TutorialActivity): self.setactivity(_ba.newactivity(TransitionActivity)) else: playerinfos: list[ba.PlayerInfo] # Generic team games. if isinstance(results, GameResults): playerinfos = results.playerinfos score = results.get_sessionteam_score(results.sessionteams[0]) fail_message = None score_order = ('decreasing' if results.lower_is_better else 'increasing') if results.scoretype in (ScoreType.SECONDS, ScoreType.MILLISECONDS): scoretype = 'time' # ScoreScreen wants hundredths of a second. if score is not None: if results.scoretype is ScoreType.SECONDS: score *= 100 elif results.scoretype is ScoreType.MILLISECONDS: score //= 10 else: raise RuntimeError('FIXME') else: if results.scoretype is not ScoreType.POINTS: print(f'Unknown ScoreType:' f' "{results.scoretype}"') scoretype = 'points' # Old coop-game-specific results; should migrate away from these. else: playerinfos = results.get('playerinfos') score = results['score'] if 'score' in results else None fail_message = (results['fail_message'] if 'fail_message' in results else None) score_order = (results['score_order'] if 'score_order' in results else 'increasing') activity_score_type = (activity.get_score_type() if isinstance( activity, CoopGameActivity) else None) assert activity_score_type is not None scoretype = activity_score_type # Validate types. if playerinfos is not None: assert isinstance(playerinfos, list) assert (isinstance(i, PlayerInfo) for i in playerinfos) # Looks like we were in a round - check the outcome and # go from there. if outcome == 'restart': # This will pop up back in the same round. self.setactivity(_ba.newactivity(TransitionActivity)) else: self.setactivity( _ba.newactivity( CoopScoreScreen, { 'playerinfos': playerinfos, 'score': score, 'fail_message': fail_message, 'score_order': score_order, 'score_type': scoretype, 'outcome': outcome, 'campaign': self.campaign, 'level': self.campaign_level_name })) # No matter what, get the next 2 levels ready to go. self._update_on_deck_game_instances()
def setup_low_life_warning_sound(self) -> None: """Set up a beeping noise to play when any players are near death.""" self._life_warning_beep = None self._life_warning_beep_timer = _ba.Timer( 1.0, WeakCall(self._update_life_warning), repeat=True)
def show_completion_banner(self, sound: bool = True) -> None: """Create the banner/sound for an acquired achievement announcement.""" from ba import _account from ba import _gameutils from bastd.actor.text import Text from bastd.actor.image import Image from ba._general import WeakCall from ba._lang import Lstr from ba._messages import DieMessage from ba._enums import TimeType, SpecialChar app = _ba.app app.last_achievement_display_time = _ba.time(TimeType.REAL) # Just piggy-back onto any current activity # (should we use the session instead?..) activity = _ba.getactivity(doraise=False) # If this gets called while this achievement is occupying a slot # already, ignore it. (probably should never happen in real # life but whatevs). if self._completion_banner_slot is not None: return if activity is None: print('show_completion_banner() called with no current activity!') return if sound: _ba.playsound(_ba.getsound('achievement'), host_only=True) else: _ba.timer( 0.5, lambda: _ba.playsound(_ba.getsound('ding'), host_only=True)) in_time = 0.300 out_time = 3.5 base_vr_depth = 200 # Find the first free slot. i = 0 while True: if i not in app.achievement_completion_banner_slots: app.achievement_completion_banner_slots.add(i) self._completion_banner_slot = i # Remove us from that slot when we close. # Use a real-timer in the UI context so the removal runs even # if our activity/session dies. with _ba.Context('ui'): _ba.timer(in_time + out_time, self._remove_banner_slot, timetype=TimeType.REAL) break i += 1 assert self._completion_banner_slot is not None y_offs = 110 * self._completion_banner_slot objs: List[ba.Actor] = [] obj = Image(_ba.gettexture('shadow'), position=(-30, 30 + y_offs), front=True, attach=Image.Attach.BOTTOM_CENTER, transition=Image.Transition.IN_BOTTOM, vr_depth=base_vr_depth - 100, transition_delay=in_time, transition_out_delay=out_time, color=(0.0, 0.1, 0, 1), scale=(1000, 300)).autoretain() objs.append(obj) assert obj.node obj.node.host_only = True obj = Image(_ba.gettexture('light'), position=(-180, 60 + y_offs), front=True, attach=Image.Attach.BOTTOM_CENTER, vr_depth=base_vr_depth, transition=Image.Transition.IN_BOTTOM, transition_delay=in_time, transition_out_delay=out_time, color=(1.8, 1.8, 1.0, 0.0), scale=(40, 300)).autoretain() objs.append(obj) assert obj.node obj.node.host_only = True obj.node.premultiplied = True combine = _ba.newnode('combine', owner=obj.node, attrs={'size': 2}) _gameutils.animate( combine, 'input0', { in_time: 0, in_time + 0.4: 30, in_time + 0.5: 40, in_time + 0.6: 30, in_time + 2.0: 0 }) _gameutils.animate( combine, 'input1', { in_time: 0, in_time + 0.4: 200, in_time + 0.5: 500, in_time + 0.6: 200, in_time + 2.0: 0 }) combine.connectattr('output', obj.node, 'scale') _gameutils.animate(obj.node, 'rotate', { 0: 0.0, 0.35: 360.0 }, loop=True) obj = Image(self.get_icon_texture(True), position=(-180, 60 + y_offs), attach=Image.Attach.BOTTOM_CENTER, front=True, vr_depth=base_vr_depth - 10, transition=Image.Transition.IN_BOTTOM, transition_delay=in_time, transition_out_delay=out_time, scale=(100, 100)).autoretain() objs.append(obj) assert obj.node obj.node.host_only = True # Flash. color = self.get_icon_color(True) combine = _ba.newnode('combine', owner=obj.node, attrs={'size': 3}) keys = { in_time: 1.0 * color[0], in_time + 0.4: 1.5 * color[0], in_time + 0.5: 6.0 * color[0], in_time + 0.6: 1.5 * color[0], in_time + 2.0: 1.0 * color[0] } _gameutils.animate(combine, 'input0', keys) keys = { in_time: 1.0 * color[1], in_time + 0.4: 1.5 * color[1], in_time + 0.5: 6.0 * color[1], in_time + 0.6: 1.5 * color[1], in_time + 2.0: 1.0 * color[1] } _gameutils.animate(combine, 'input1', keys) keys = { in_time: 1.0 * color[2], in_time + 0.4: 1.5 * color[2], in_time + 0.5: 6.0 * color[2], in_time + 0.6: 1.5 * color[2], in_time + 2.0: 1.0 * color[2] } _gameutils.animate(combine, 'input2', keys) combine.connectattr('output', obj.node, 'color') obj = Image(_ba.gettexture('achievementOutline'), model_transparent=_ba.getmodel('achievementOutline'), position=(-180, 60 + y_offs), front=True, attach=Image.Attach.BOTTOM_CENTER, vr_depth=base_vr_depth, transition=Image.Transition.IN_BOTTOM, transition_delay=in_time, transition_out_delay=out_time, scale=(100, 100)).autoretain() assert obj.node obj.node.host_only = True # Flash. color = (2, 1.4, 0.4, 1) combine = _ba.newnode('combine', owner=obj.node, attrs={'size': 3}) keys = { in_time: 1.0 * color[0], in_time + 0.4: 1.5 * color[0], in_time + 0.5: 6.0 * color[0], in_time + 0.6: 1.5 * color[0], in_time + 2.0: 1.0 * color[0] } _gameutils.animate(combine, 'input0', keys) keys = { in_time: 1.0 * color[1], in_time + 0.4: 1.5 * color[1], in_time + 0.5: 6.0 * color[1], in_time + 0.6: 1.5 * color[1], in_time + 2.0: 1.0 * color[1] } _gameutils.animate(combine, 'input1', keys) keys = { in_time: 1.0 * color[2], in_time + 0.4: 1.5 * color[2], in_time + 0.5: 6.0 * color[2], in_time + 0.6: 1.5 * color[2], in_time + 2.0: 1.0 * color[2] } _gameutils.animate(combine, 'input2', keys) combine.connectattr('output', obj.node, 'color') objs.append(obj) objt = Text(Lstr(value='${A}:', subs=[('${A}', Lstr(resource='achievementText'))]), position=(-120, 91 + y_offs), front=True, v_attach=Text.VAttach.BOTTOM, vr_depth=base_vr_depth - 10, transition=Text.Transition.IN_BOTTOM, flatness=0.5, transition_delay=in_time, transition_out_delay=out_time, color=(1, 1, 1, 0.8), scale=0.65).autoretain() objs.append(objt) assert objt.node objt.node.host_only = True objt = Text(self.display_name, position=(-120, 50 + y_offs), front=True, v_attach=Text.VAttach.BOTTOM, transition=Text.Transition.IN_BOTTOM, vr_depth=base_vr_depth, flatness=0.5, transition_delay=in_time, transition_out_delay=out_time, flash=True, color=(1, 0.8, 0, 1.0), scale=1.5).autoretain() objs.append(objt) assert objt.node objt.node.host_only = True objt = Text(_ba.charstr(SpecialChar.TICKET), position=(-120 - 170 + 5, 75 + y_offs - 20), front=True, v_attach=Text.VAttach.BOTTOM, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, transition=Text.Transition.IN_BOTTOM, vr_depth=base_vr_depth, transition_delay=in_time, transition_out_delay=out_time, flash=True, color=(0.5, 0.5, 0.5, 1), scale=3.0).autoretain() objs.append(objt) assert objt.node objt.node.host_only = True objt = Text('+' + str(self.get_award_ticket_value()), position=(-120 - 180 + 5, 80 + y_offs - 20), v_attach=Text.VAttach.BOTTOM, front=True, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, transition=Text.Transition.IN_BOTTOM, vr_depth=base_vr_depth, flatness=0.5, shadow=1.0, transition_delay=in_time, transition_out_delay=out_time, flash=True, color=(0, 1, 0, 1), scale=1.5).autoretain() objs.append(objt) assert objt.node objt.node.host_only = True # Add the 'x 2' if we've got pro. if _account.have_pro(): objt = Text('x 2', position=(-120 - 180 + 45, 80 + y_offs - 50), v_attach=Text.VAttach.BOTTOM, front=True, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, transition=Text.Transition.IN_BOTTOM, vr_depth=base_vr_depth, flatness=0.5, shadow=1.0, transition_delay=in_time, transition_out_delay=out_time, flash=True, color=(0.4, 0, 1, 1), scale=0.9).autoretain() objs.append(objt) assert objt.node objt.node.host_only = True objt = Text(self.description_complete, position=(-120, 30 + y_offs), front=True, v_attach=Text.VAttach.BOTTOM, transition=Text.Transition.IN_BOTTOM, vr_depth=base_vr_depth - 10, flatness=0.5, transition_delay=in_time, transition_out_delay=out_time, color=(1.0, 0.7, 0.5, 1.0), scale=0.8).autoretain() objs.append(objt) assert objt.node objt.node.host_only = True for actor in objs: _ba.timer(out_time + 1.000, WeakCall(actor.handlemessage, DieMessage()))