def show_post_purchase_message() -> None: """(internal)""" from ba._lang import Lstr from ba._enums import TimeType app = _ba.app cur_time = _ba.time(TimeType.REAL) if (app.last_post_purchase_message_time is None or cur_time - app.last_post_purchase_message_time > 3.0): app.last_post_purchase_message_time = cur_time with _ba.Context('ui'): _ba.screenmessage(Lstr(resource='updatingAccountText', fallback_resource='purchasingText'), color=(0, 1, 0)) _ba.playsound(_ba.getsound('click01'))
def print_corrupt_file_error() -> None: """Print an error if a corrupt file is found.""" from ba._lang import get_resource from ba._general import Call from ba._enums import TimeType _ba.timer( 2.0, lambda: _ba.screenmessage(get_resource('internal.corruptFileText'). replace('${EMAIL}', '*****@*****.**'), color=(1, 0, 0)), timetype=TimeType.REAL) _ba.timer(2.0, Call(_ba.playsound, _ba.getsound('error')), timetype=TimeType.REAL)
def handlemessage(self, msg: Any) -> Any: """Standard generic message handler.""" if isinstance(msg, ChangeMessage): # If we've been removed from the lobby, ignore this stuff. if self._dead: from ba import _error _error.print_error('chooser got ChangeMessage after dying') return if not self._text_node: from ba import _error _error.print_error('got ChangeMessage after nodes died') return if msg.what == 'team': teams = self.lobby.teams if len(teams) > 1: _ba.playsound(self._swish_sound) self._selected_team_index = ( (self._selected_team_index + msg.value) % len(teams)) self._update_text() self.update_position() self._update_icon() elif msg.what == 'profileindex': if len(self._profilenames) == 1: # This should be pretty hard to hit now with # automatic local accounts. _ba.playsound(_ba.getsound('error')) else: # Pick the next player profile and assign our name # and character based on that. _ba.playsound(self._deek_sound) self._profileindex = ((self._profileindex + msg.value) % len(self._profilenames)) self.update_from_player_profiles() elif msg.what == 'character': _ba.playsound(self._click_sound) # update our index in our local list of characters self.character_index = ((self.character_index + msg.value) % len(self.character_names)) self._update_text() self._update_icon() elif msg.what == 'ready': self._handle_ready_msg(bool(msg.value))
def on_player_request(self, player: ba.SessionPlayer) -> bool: """Called when a new ba.Player wants to join the Session. This should return True or False to accept/reject. """ # Limit player counts *unless* we're in a stress test. if _ba.app.stress_test_reset_timer is None: if len(self.sessionplayers) >= self.max_players: # Print a rejection message *only* to the client trying to # join (prevents spamming everyone else in the game). _ba.playsound(_ba.getsound('error')) _ba.screenmessage(Lstr(resource='playerLimitReachedText', subs=[('${COUNT}', str(self.max_players))]), color=(0.8, 0.0, 0.0), clients=[player.inputdevice.client_id], transient=True) return False _ba.playsound(_ba.getsound('dripity')) return True
def handle_scan_results(results: ScanResults) -> None: """Called in the game thread with results of a completed scan.""" from ba import _lang # Warnings generally only get printed locally for users' benefit # (things like out-of-date scripts being ignored, etc.) # Errors are more serious and will get included in the regular log # warnings = results.get('warnings', '') # errors = results.get('errors', '') if results.warnings != '' or results.errors != '': _ba.screenmessage(_lang.Lstr(resource='scanScriptsErrorText'), color=(1, 0, 0)) _ba.playsound(_ba.getsound('error')) if results.warnings != '': _ba.log(results.warnings, to_server=False) if results.errors != '': _ba.log(results.errors)
def show_user_scripts() -> None: """Open or nicely print the location of the user-scripts directory.""" from ba import _lang from ba._enums import Permission app = _ba.app # First off, if we need permission for this, ask for it. if not _ba.have_permission(Permission.STORAGE): _ba.playsound(_ba.getsound('error')) _ba.screenmessage(_lang.Lstr(resource='storagePermissionAccessText'), color=(1, 0, 0)) _ba.request_permission(Permission.STORAGE) return # Secondly, if the dir doesn't exist, attempt to make it. if not os.path.exists(app.python_directory_user): os.makedirs(app.python_directory_user) # On android, attempt to write a file in their user-scripts dir telling # them about modding. This also has the side-effect of allowing us to # media-scan that dir so it shows up in android-file-transfer, since it # doesn't seem like there's a way to inform the media scanner of an empty # directory, which means they would have to reboot their device before # they can see it. if app.platform == 'android': try: usd: Optional[str] = app.python_directory_user if usd is not None and os.path.isdir(usd): file_name = usd + '/about_this_folder.txt' with open(file_name, 'w') as outfile: outfile.write('You can drop files in here to mod the game.' ' See settings/advanced' ' in the game for more info.') _ba.android_media_scan_file(file_name) except Exception: from ba import _error _error.print_exception('error writing about_this_folder stuff') # On a few platforms we try to open the dir in the UI. if app.platform in ['mac', 'windows']: _ba.open_dir_externally(app.python_directory_user) # Otherwise we just print a pretty version of it. else: _ba.screenmessage(get_human_readable_user_scripts_path())
def announce_game_results(self, activity: ba.GameActivity, results: ba.GameResults, delay: float, announce_winning_team: bool = True) -> None: """Show basic game result at the end of a game. (before transitioning to a score screen). This will include a zoom-text of 'BLUE WINS' or whatnot, along with a possible audio announcement of the same. """ # pylint: disable=cyclic-import # pylint: disable=too-many-locals from ba._math import normalized_color from ba._general import Call from ba._gameutils import cameraflash from ba._language import Lstr from ba._freeforallsession import FreeForAllSession from ba._messages import CelebrateMessage _ba.timer(delay, Call(_ba.playsound, _ba.getsound('boxingBell'))) if announce_winning_team: winning_sessionteam = results.winning_sessionteam if winning_sessionteam is not None: # Have all players celebrate. celebrate_msg = CelebrateMessage(duration=10.0) assert winning_sessionteam.activityteam is not None for player in winning_sessionteam.activityteam.players: if player.actor: player.actor.handlemessage(celebrate_msg) cameraflash() # Some languages say "FOO WINS" different for teams vs players. if isinstance(self, FreeForAllSession): wins_resource = 'winsPlayerText' else: wins_resource = 'winsTeamText' wins_text = Lstr(resource=wins_resource, subs=[('${NAME}', winning_sessionteam.name)]) activity.show_zoom_message( wins_text, scale=0.85, color=normalized_color(winning_sessionteam.color), )
def end( # type: ignore self, results: Any = None, announce_winning_team: bool = True, announce_delay: float = 0.1, force: bool = False) -> None: """ End the game and announce the single winning team unless 'announce_winning_team' is False. (for results without a single most-important winner). """ # pylint: disable=arguments-differ from ba._coopsession import CoopSession from ba._multiteamsession import MultiTeamSession from ba._general import Call # Announce win (but only for the first finish() call) # (also don't announce in co-op sessions; we leave that up to them). session = self.session if not isinstance(session, CoopSession): do_announce = not self.has_ended() super().end(results, delay=2.0 + announce_delay, force=force) # Need to do this *after* end end call so that results is valid. assert isinstance(results, TeamGameResults) if do_announce and isinstance(session, MultiTeamSession): session.announce_game_results( self, results, delay=announce_delay, announce_winning_team=announce_winning_team) # For co-op we just pass this up the chain with a delay added # (in most cases). Team games expect a delay for the announce # portion in teams/ffa mode so this keeps it consistent. else: # don't want delay on restarts.. if (isinstance(results, dict) and 'outcome' in results and results['outcome'] == 'restart'): delay = 0.0 else: delay = 2.0 _ba.timer(0.1, Call(_ba.playsound, _ba.getsound('boxingBell'))) super().end(results, delay=delay, force=force)
def resume(self) -> None: """Resume the game due to a user request or menu closing. If there's a foreground host-activity that's currently paused, tell it to resume. """ # FIXME: Shouldn't be touching scene stuff here; # should just pass the request on to the host-session. activity = _ba.get_foreground_host_activity() if activity is not None: with _ba.Context(activity): globs = activity.globalsnode if globs.paused: _ba.playsound(_ba.getsound('refWhistle')) globs.paused = False # FIXME: This should not be an actor attr. activity.paused_text = None
def _on_player_ready(self, chooser: ba.Chooser) -> None: """Called when a ba.Player has checked themself ready.""" lobby = chooser.lobby activity = self._activity_weak() # This happens sometimes. That seems like it shouldn't be happening; # when would we have a session and a chooser with players but no # active activity? if activity is None: print('_on_player_ready called with no activity.') return # In joining-activities, we wait till all choosers are ready # and then create all players at once. if activity.is_joining_activity: if not lobby.check_all_ready(): return choosers = lobby.get_choosers() min_players = self.min_players if len(choosers) >= min_players: for lch in lobby.get_choosers(): self._add_chosen_player(lch) lobby.remove_all_choosers() # Get our next activity going. self._complete_end_activity(activity, {}) else: _ba.screenmessage( Lstr(resource='notEnoughPlayersText', subs=[('${COUNT}', str(min_players))]), color=(1, 1, 0), ) _ba.playsound(_ba.getsound('error')) # Otherwise just add players on the fly. else: self._add_chosen_player(chooser) lobby.remove_chooser(chooser.getplayer())
def handle_deep_link(self, url: str) -> None: """Handle a deep link URL.""" from ba._lang import Lstr from ba._enums import TimeType appname = _ba.appname() if url.startswith(f'{appname}://code/'): code = url.replace(f'{appname}://code/', '') # If we're not signed in, queue up the code to run the next time we # are and issue a warning if we haven't signed in within the next # few seconds. if _ba.get_account_state() != 'signed_in': def check_pending_codes() -> None: """(internal)""" # If we're still not signed in and have pending codes, # inform the user that they need to sign in to use them. if _ba.app.pending_promo_codes: _ba.screenmessage( Lstr(resource='signInForPromoCodeText'), color=(1, 0, 0)) _ba.playsound(_ba.getsound('error')) _ba.app.pending_promo_codes.append(code) _ba.timer(6.0, check_pending_codes, timetype=TimeType.REAL) return _ba.screenmessage(Lstr(resource='submittingPromoCodeText'), color=(0, 1, 0)) _ba.add_transaction({ 'type': 'PROMO_CODE', 'expire_time': time.time() + 5, 'code': code }) _ba.run_transactions() else: _ba.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0)) _ba.playsound(_ba.getsound('error'))
def __init__(self, settings: dict): """Instantiate the Activity.""" super().__init__(settings) # Holds some flattened info about the player set at the point # when on_begin() is called. self.initialplayerinfos: Optional[List[ba.PlayerInfo]] = None # Go ahead and get our map loading. self._map_type = _map.get_map_class(self._calc_map_name(settings)) self._spawn_sound = _ba.getsound('spawn') self._map_type.preload() self._map: Optional[ba.Map] = None self._powerup_drop_timer: Optional[ba.Timer] = None self._tnt_spawners: Optional[Dict[int, TNTSpawner]] = None self._tnt_drop_timer: Optional[ba.Timer] = None self._game_scoreboard_name_text: Optional[ba.Actor] = None self._game_scoreboard_description_text: Optional[ba.Actor] = None self._standard_time_limit_time: Optional[int] = None self._standard_time_limit_timer: Optional[ba.Timer] = None self._standard_time_limit_text: Optional[ba.NodeActor] = None self._standard_time_limit_text_input: Optional[ba.NodeActor] = None self._tournament_time_limit: Optional[int] = None self._tournament_time_limit_timer: Optional[ba.Timer] = None self._tournament_time_limit_title_text: Optional[ba.NodeActor] = None self._tournament_time_limit_text: Optional[ba.NodeActor] = None self._tournament_time_limit_text_input: Optional[ba.NodeActor] = None self._zoom_message_times: Dict[int, float] = {} self._is_waiting_for_continue = False self._continue_cost = _ba.get_account_misc_read_val( 'continueStartCost', 25) self._continue_cost_mult = _ba.get_account_misc_read_val( 'continuesMult', 2) self._continue_cost_offset = _ba.get_account_misc_read_val( 'continuesOffset', 0)
def purchase_not_valid_error() -> None: from ba._language import Lstr _ba.playsound(_ba.getsound('error')) _ba.screenmessage(Lstr(resource='store.purchaseNotValidError', subs=[('${EMAIL}', '*****@*****.**')]), color=(1, 0, 0))
def on_player_leave(self, player: ba.Player) -> None: """Called when a previously-accepted ba.Player leaves the session.""" # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=cyclic-import from ba._freeforallsession import FreeForAllSession from ba._lang import Lstr from ba import _error # Remove them from the game rosters. if player in self.players: _ba.playsound(_ba.getsound('playerLeft')) team: Optional[ba.Team] # The player will have no team if they are still in the lobby. try: team = player.team except _error.TeamNotFoundError: team = None activity = self._activity_weak() # If he had no team, he's in the lobby. # If we have a current activity with a lobby, ask them to # remove him. if team is None: with _ba.Context(self): try: self.lobby.remove_chooser(player) except Exception: _error.print_exception( 'Error in Lobby.remove_chooser()') # *If* they were actually in the game, announce their departure. if team is not None: _ba.screenmessage( Lstr(resource='playerLeftText', subs=[('${PLAYER}', player.get_name(full=True))])) # Remove him from his team and session lists. # (he may not be on the team list since player are re-added to # team lists every activity) if team is not None and player in team.players: # Testing; can remove this eventually. if isinstance(self, FreeForAllSession): if len(team.players) != 1: _error.print_error('expected 1 player in FFA team') team.players.remove(player) # Remove player from any current activity. if activity is not None and player in activity.players: activity.players.remove(player) # Run the activity callback unless its been expired. if not activity.is_expired(): try: with _ba.Context(activity): activity.on_player_leave(player) except Exception: _error.print_exception( 'exception in on_player_leave for activity', activity) else: _error.print_error('expired activity in on_player_leave;' " shouldn't happen") player.set_activity(None) player.set_node(None) # Reset the player; this will remove its actor-ref and clear # its calls/etc try: with _ba.Context(activity): player.reset() except Exception: _error.print_exception( 'exception in player.reset in' ' on_player_leave for player', player) # If we're a non-team session, remove the player's team completely. if not self._use_teams and team is not None: # If the team's in an activity, call its on_team_leave # callback. if activity is not None and team in activity.teams: activity.teams.remove(team) if not activity.is_expired(): try: with _ba.Context(activity): activity.on_team_leave(team) except Exception: _error.print_exception( 'exception in on_team_leave for activity', activity) else: _error.print_error( 'expired activity in on_player_leave p2' "; shouldn't happen") # Clear the team's game-data (so dying stuff will # have proper context). try: with _ba.Context(activity): team.reset_gamedata() except Exception: _error.print_exception( 'exception clearing gamedata for team:', team, 'for player:', player, 'in activity:', activity) # Remove the team from the session. self.teams.remove(team) try: with _ba.Context(self): self.on_team_leave(team) except Exception: _error.print_exception( 'exception in on_team_leave for session', self) # Clear the team's session-data (so dying stuff will # have proper context). try: with _ba.Context(self): team.reset_sessiondata() except Exception: _error.print_exception( 'exception clearing sessiondata for team:', team, 'in session:', self) # Now remove them from the session list. self.players.remove(player) else: print('ERROR: Session.on_player_leave called' ' for player not in our list.')
def handle_scan_results(self, results: ScanResults) -> None: """Called in the game thread with results of a completed scan.""" from ba._language import Lstr from ba._plugin import PotentialPlugin # Warnings generally only get printed locally for users' benefit # (things like out-of-date scripts being ignored, etc.) # Errors are more serious and will get included in the regular log # warnings = results.get('warnings', '') # errors = results.get('errors', '') if results.warnings != '' or results.errors != '': import textwrap _ba.screenmessage(Lstr(resource='scanScriptsErrorText'), color=(1, 0, 0)) _ba.playsound(_ba.getsound('error')) if results.warnings != '': _ba.log(textwrap.indent(results.warnings, 'Warning (meta-scan): '), to_server=False) if results.errors != '': _ba.log(textwrap.indent(results.errors, 'Error (meta-scan): ')) # Handle plugins. plugs = _ba.app.plugins config_changed = False found_new = False plugstates: Dict[str, Dict] = _ba.app.config.setdefault('Plugins', {}) assert isinstance(plugstates, dict) # Create a potential-plugin for each class we found in the scan. for class_path in results.plugins: plugs.potential_plugins.append( PotentialPlugin(display_name=Lstr(value=class_path), class_path=class_path, available=True)) if class_path not in plugstates: if _ba.app.headless_mode: # If we running in headless mode, enable plugin by default # to allow server admins to get their modified build # working 'out-of-the-box', without manually updating the # config. plugstates[class_path] = {'enabled': True} else: # If we running in normal mode, disable plugin by default # (user can enable it later). plugstates[class_path] = {'enabled': False} config_changed = True found_new = True # Also add a special one for any plugins set to load but *not* found # in the scan (this way they will show up in the UI so we can disable # them) for class_path, plugstate in plugstates.items(): enabled = plugstate.get('enabled', False) assert isinstance(enabled, bool) if enabled and class_path not in results.plugins: plugs.potential_plugins.append( PotentialPlugin(display_name=Lstr(value=class_path), class_path=class_path, available=False)) plugs.potential_plugins.sort(key=lambda p: p.class_path) if found_new: _ba.screenmessage(Lstr(resource='pluginsDetectedText'), color=(0, 1, 0)) _ba.playsound(_ba.getsound('ding')) if config_changed: _ba.app.config.commit()
def __init__(self, vpos: float, sessionplayer: _ba.SessionPlayer, lobby: 'Lobby') -> None: self._deek_sound = _ba.getsound('deek') self._click_sound = _ba.getsound('click01') self._punchsound = _ba.getsound('punch01') self._swish_sound = _ba.getsound('punchSwish') self._errorsound = _ba.getsound('error') self._mask_texture = _ba.gettexture('characterIconMask') self._vpos = vpos self._lobby = weakref.ref(lobby) self._sessionplayer = sessionplayer self._inited = False self._dead = False self._text_node: Optional[ba.Node] = None self._profilename = '' self._profilenames: List[str] = [] self._ready: bool = False self._character_names: List[str] = [] self._last_change: Sequence[Union[float, int]] = (0, 0) self._profiles: Dict[str, Dict[str, Any]] = {} app = _ba.app # Load available player profiles either from the local config or # from the remote device. self.reload_profiles() # Note: this is just our local index out of available teams; *not* # the team-id! self._selected_team_index: int = self.lobby.next_add_team # Store a persistent random character index and colors; we'll use this # for the '_random' profile. Let's use their input_device id to seed # it. This will give a persistent character for them between games # and will distribute characters nicely if everyone is random. self._random_color, self._random_highlight = ( get_player_profile_colors(None)) # To calc our random character we pick a random one out of our # unlocked list and then locate that character's index in the full # list. char_index_offset = app.lobby_random_char_index_offset self._random_character_index = ( (sessionplayer.inputdevice.id + char_index_offset) % len(self._character_names)) # Attempt to set an initial profile based on what was used previously # for this input-device, etc. self._profileindex = self._select_initial_profile() self._profilename = self._profilenames[self._profileindex] self._text_node = _ba.newnode('text', delegate=self, attrs={ 'position': (-100, self._vpos), 'maxwidth': 160, 'shadow': 0.5, 'vr_depth': -20, 'h_align': 'left', 'v_align': 'center', 'v_attach': 'top' }) animate(self._text_node, 'scale', {0: 0, 0.1: 1.0}) self.icon = _ba.newnode('image', owner=self._text_node, attrs={ 'position': (-130, self._vpos + 20), 'mask_texture': self._mask_texture, 'vr_depth': -10, 'attach': 'topCenter' }) animate_array(self.icon, 'scale', 2, {0: (0, 0), 0.1: (45, 45)}) # Set our initial name to '<choosing player>' in case anyone asks. self._sessionplayer.setname( Lstr(resource='choosingPlayerText').evaluate(), real=False) # Init these to our rando but they should get switched to the # selected profile (if any) right after. self._character_index = self._random_character_index self._color = self._random_color self._highlight = self._random_highlight self.update_from_profile() self.update_position() self._inited = True self._set_ready(False)
def _load_activity_media(self) -> None: self.orchestrahitsound1 = _ba.getsound('orchestraHit') self.orchestrahitsound2 = _ba.getsound('orchestraHit2') self.orchestrahitsound3 = _ba.getsound('orchestraHit3') self.orchestrahitsound4 = _ba.getsound('orchestraHit4')
def on_player_leave(self, sessionplayer: ba.SessionPlayer) -> None: """Called when a previously-accepted ba.SessionPlayer leaves.""" # pylint: disable=too-many-branches if sessionplayer not in self.players: print('ERROR: Session.on_player_leave called' ' for player not in our list.') return _ba.playsound(_ba.getsound('playerLeft')) activity = self._activity_weak() if not sessionplayer.in_game: # Ok, the player's still in the lobby. Simply remove them from it. with _ba.Context(self): try: self.lobby.remove_chooser(sessionplayer) except Exception: print_exception('Error in Lobby.remove_chooser().') else: # Ok, they've already entered the game. Remove them from # teams/activities/etc. sessionteam = sessionplayer.team assert sessionteam is not None assert sessionplayer in sessionteam.players _ba.screenmessage( Lstr(resource='playerLeftText', subs=[('${PLAYER}', sessionplayer.get_name(full=True))])) # Remove them from their SessionTeam. if sessionplayer in sessionteam.players: sessionteam.players.remove(sessionplayer) else: print('SessionPlayer not found in SessionTeam' ' in on_player_leave.') # Grab their activity-specific player instance. player = sessionplayer.gameplayer assert isinstance(player, (Player, type(None))) # Remove them from any current Activity. if activity is not None: if player in activity.players: activity.remove_player(sessionplayer) else: print('Player not found in Activity in on_player_leave.') # If we're a non-team session, remove their team too. if not self.use_teams: # They should have been the only one on their team. assert not sessionteam.players # Remove their Team from the Activity. if activity is not None: if sessionteam.gameteam in activity.teams: activity.remove_team(sessionteam) else: print('Team not found in Activity in on_player_leave.') # And then from the Session. with _ba.Context(self): if sessionteam in self.teams: try: self.teams.remove(sessionteam) self.on_team_leave(sessionteam) except Exception: print_exception( f'Error in on_team_leave for Session {self}.') else: print('Team no in Session teams in on_player_leave.') try: sessionteam.reset_sessiondata() except Exception: print_exception( f'Error clearing sessiondata' f' for team {sessionteam} in session {self}.') # Now remove them from the session list. self.players.remove(sessionplayer)
def purchase_already_in_progress_error() -> None: from ba._language import Lstr _ba.playsound(_ba.getsound('error')) _ba.screenmessage(Lstr(resource='store.purchaseAlreadyInProgressText'), color=(1, 0, 0))
def _show_info(self) -> None: """Show the game description.""" from ba._gameutils import animate from bastd.actor.zoomtext import ZoomText name = self.get_instance_display_string() ZoomText(name, maxwidth=800, lifespan=2.5, jitter=2.0, position=(0, 180), flash=False, color=(0.93 * 1.25, 0.9 * 1.25, 1.0 * 1.25), trailcolor=(0.15, 0.05, 1.0, 0.0)).autoretain() _ba.timer(0.2, Call(_ba.playsound, _ba.getsound('gong'))) # The description can be either a string or a sequence with args # to swap in post-translation. desc_in = self.get_instance_description() desc_l: Sequence if isinstance(desc_in, str): desc_l = [desc_in] # handle simple string case else: desc_l = desc_in if not isinstance(desc_l[0], str): raise TypeError('Invalid format for instance description') subs = [] for i in range(len(desc_l) - 1): subs.append(('${ARG' + str(i + 1) + '}', str(desc_l[i + 1]))) translation = Lstr(translate=('gameDescriptions', desc_l[0]), subs=subs) # Do some standard filters (epic mode, etc). if self.settings_raw.get('Epic Mode', False): translation = Lstr(resource='epicDescriptionFilterText', subs=[('${DESCRIPTION}', translation)]) vrmode = _ba.app.vr_mode dnode = _ba.newnode('text', attrs={ 'v_attach': 'center', 'h_attach': 'center', 'h_align': 'center', 'color': (1, 1, 1, 1), 'shadow': 1.0 if vrmode else 0.5, 'flatness': 1.0 if vrmode else 0.5, 'vr_depth': -30, 'position': (0, 80), 'scale': 1.2, 'maxwidth': 700, 'text': translation }) cnode = _ba.newnode('combine', owner=dnode, attrs={ 'input0': 1.0, 'input1': 1.0, 'input2': 1.0, 'size': 4 }) cnode.connectattr('output', dnode, 'color') keys = {0.5: 0, 1.0: 1.0, 2.5: 1.0, 4.0: 0.0} animate(cnode, 'input3', keys) _ba.timer(4.0, dnode.delete)
def ui_remote_press() -> None: """Handle a press by a remote device that is only usable for nav.""" from ba._lang import Lstr _ba.screenmessage(Lstr(resource="internal.controllerForMenusOnlyText"), color=(1, 0, 0)) _ba.playsound(_ba.getsound('error'))
def error_message() -> None: from ba._language import Lstr _ba.playsound(_ba.getsound('error')) _ba.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
def in_progress_message() -> None: from ba._language import Lstr _ba.playsound(_ba.getsound('error')) _ba.screenmessage(Lstr(resource='getTicketsWindow.inProgressText'), color=(1, 0, 0))
def __init__(self, vpos: float, player: _ba.SessionPlayer, lobby: 'Lobby') -> None: # FIXME: Tidy up around here. # pylint: disable=too-many-branches # pylint: disable=too-many-statements from ba import _gameutils from ba import _profile from ba import _lang app = _ba.app self._deek_sound = _ba.getsound('deek') self._click_sound = _ba.getsound('click01') self._punchsound = _ba.getsound('punch01') self._swish_sound = _ba.getsound('punchSwish') self._errorsound = _ba.getsound('error') self._mask_texture = _ba.gettexture('characterIconMask') self._vpos = vpos self._lobby = weakref.ref(lobby) self._player = player self._inited = False self._dead = False self._text_node: Optional[ba.Node] = None self._profilename = '' self._profilenames: List[str] = [] self._ready: bool = False self.character_names: List[str] = [] self.last_change: Sequence[Union[float, int]] = (0, 0) # Hmm does this need to be public? self.profiles: Dict[str, Dict[str, Any]] = {} # Load available profiles either from the local config or from the # remote device. self.reload_profiles() # Note: this is just our local index out of available teams; *not* # the team-id! self._selected_team_index: int = self.lobby.next_add_team # Store a persistent random character index; we'll use this for the # '_random' profile. Let's use their input_device id to seed it. This # will give a persistent character for them between games and will # distribute characters nicely if everyone is random. try: input_device_id = self._player.get_input_device().id except Exception: from ba import _error _error.print_exception('Error getting device-id on chooser create') input_device_id = 0 if app.lobby_random_char_index_offset is None: # We want the first device that asks for a chooser to always get # spaz as a random character.. # scratch that.. we now kinda accomplish the same thing with # account profiles so lets just be fully random here. app.lobby_random_char_index_offset = (random.randrange(1000)) # To calc our random index we pick a random character out of our # unlocked list and then locate that character's index in the full # list. char_index_offset = app.lobby_random_char_index_offset assert char_index_offset is not None self._random_character_index = ((input_device_id + char_index_offset) % len(self.character_names)) self._random_color, self._random_highlight = ( _profile.get_player_profile_colors(None)) # Attempt to pick an initial profile based on what's been stored # for this input device. input_device = self._player.get_input_device() try: name = input_device.name unique_id = input_device.unique_identifier self._profilename = ( app.config['Default Player Profiles'][name + ' ' + unique_id]) self._profileindex = self._profilenames.index(self._profilename) # If this one is __account__ and is local and we haven't marked # anyone as the account-profile device yet, mark this guy as it. # (prevents the next joiner from getting the account profile too). if (self._profilename == '__account__' and not input_device.is_remote_client and app.lobby_account_profile_device_id is None): app.lobby_account_profile_device_id = input_device_id # Well hmm that didn't work.. pick __account__, _random, or some # other random profile. except Exception: profilenames = self._profilenames # We want the first local input-device in the game to latch on to # the account profile. if (not input_device.is_remote_client and not input_device.is_controller_app): if (app.lobby_account_profile_device_id is None and '__account__' in profilenames): app.lobby_account_profile_device_id = input_device_id # If this is the designated account-profile-device, try to default # to the account profile. if (input_device_id == app.lobby_account_profile_device_id and '__account__' in profilenames): self._profileindex = profilenames.index('__account__') else: # If this is the controller app, it defaults to using a random # profile (since we can pull the random name from the app). if input_device.is_controller_app: self._profileindex = profilenames.index('_random') else: # If its a client connection, for now just force # the account profile if possible.. (need to provide a # way for clients to specify/remember their default # profile on remote servers that do not already know them). if (input_device.is_remote_client and '__account__' in profilenames): self._profileindex = profilenames.index('__account__') else: # Cycle through our non-random profiles once; after # that, everyone gets random. while (app.lobby_random_profile_index < len(profilenames) and profilenames[app.lobby_random_profile_index] in ('_random', '__account__', '_edit')): app.lobby_random_profile_index += 1 if (app.lobby_random_profile_index < len(profilenames)): self._profileindex = ( app.lobby_random_profile_index) app.lobby_random_profile_index += 1 else: self._profileindex = profilenames.index('_random') self._profilename = profilenames[self._profileindex] self.character_index = self._random_character_index self._color = self._random_color self._highlight = self._random_highlight self._text_node = _ba.newnode('text', delegate=self, attrs={ 'position': (-100, self._vpos), 'maxwidth': 160, 'shadow': 0.5, 'vr_depth': -20, 'h_align': 'left', 'v_align': 'center', 'v_attach': 'top' }) _gameutils.animate(self._text_node, 'scale', {0: 0, 0.1: 1.0}) self.icon = _ba.newnode('image', owner=self._text_node, attrs={ 'position': (-130, self._vpos + 20), 'mask_texture': self._mask_texture, 'vr_depth': -10, 'attach': 'topCenter' }) _gameutils.animate_array(self.icon, 'scale', 2, { 0: (0, 0), 0.1: (45, 45) }) self._set_ready(False) # Set our initial name to '<choosing player>' in case anyone asks. self._player.set_name( _lang.Lstr(resource='choosingPlayerText').evaluate(), real=False) self.update_from_player_profiles() self.update_position() self._inited = True
def play_gong_sound() -> None: _ba.playsound(_ba.getsound('gong'))
def gear_vr_controller_warning() -> None: from ba._language import Lstr _ba.playsound(_ba.getsound('error')) _ba.screenmessage(Lstr(resource='usesExternalControllerText'), color=(1, 0, 0))
def temporarily_unavailable_message() -> None: from ba._language import Lstr _ba.playsound(_ba.getsound('error')) _ba.screenmessage( Lstr(resource='getTicketsWindow.unavailableTemporarilyText'), color=(1, 0, 0))
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()))