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 self.pending_promo_codes: _ba.screenmessage(Lstr(resource='signInForPromoCodeText'), color=(1, 0, 0)) _ba.playsound(_ba.getsound('error'))
def ui_remote_press() -> None: """Handle a press by a remote device that is only usable for nav.""" from ba._lang import Lstr # Can be called without a context; need a context for getsound. with _ba.Context('ui'): _ba.screenmessage(Lstr(resource='internal.controllerForMenusOnlyText'), color=(1, 0, 0)) _ba.playsound(_ba.getsound('error'))
def handle_account_gained_tickets(count: int) -> None: """Called when the current account has been awarded tickets. (internal) """ from ba._lang import Lstr _ba.screenmessage(Lstr(resource='getTicketsWindow.receivedTicketsText', subs=[('${COUNT}', str(count))]), color=(0, 1, 0)) _ba.playsound(_ba.getsound('cashRegister'))
def handle_deep_link(self, url: str) -> None: """Handle a deep link URL.""" from ba._language import Lstr appname = _ba.appname() if url.startswith(f'{appname}://code/'): code = url.replace(f'{appname}://code/', '') self.accounts.add_pending_promo_code(code) else: _ba.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0)) _ba.playsound(_ba.getsound('error'))
def spawn_player_spaz(self, player: PlayerType, position: Sequence[float] = (0, 0, 0), angle: float = None) -> MyPlayerSpaz: """Create and wire up a ba.PlayerSpaz for the provided ba.Player.""" # pylint: disable=too-many-locals # pylint: disable=cyclic-import name = player.getname() color = player.color highlight = player.highlight light_color = _math.normalized_color(color) display_color = _ba.safecolor(color, target_intensity=0.75) spaz = MyPlayerSpaz(color=color, highlight=highlight, character=player.character, player=player) player.actor = spaz assert spaz.node if position is None: # In teams-mode get our team-start-location. if isinstance(self.session, DualTeamSession): position = (self.map.get_start_position(player.team.id)) else: # Otherwise do free-for-all spawn locations. position = self.map.get_ffa_start_position(self.players) # If this is co-op and we're on Courtyard or Runaround, add the # material that allows us to collide with the player-walls. # FIXME: Need to generalize this. if isinstance(self.session, CoopSession) and self.map.getname() in [ 'Courtyard', 'Tower D' ]: mat = self.map.preloaddata['collide_with_wall_material'] assert isinstance(spaz.node.materials, tuple) assert isinstance(spaz.node.roller_materials, tuple) spaz.node.materials += (mat, ) spaz.node.roller_materials += (mat, ) spaz.node.name = name spaz.node.name_color = display_color spaz.connect_controls_to_player() # Move to the stand position and add a flash of light. spaz.handlemessage( StandMessage( position, angle if angle is not None else random.uniform(0, 360))) _ba.playsound(self._spawn_sound, 1, position=spaz.node.position) light = _ba.newnode('light', attrs={'color': light_color}) spaz.node.connectattr('position', light, 'position') animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) _ba.timer(0.5, light.delete) return spaz
def party_icon_activate(origin: Sequence[float]) -> None: import weakref from bastd.ui.party import PartyWindow app = _ba.app _ba.playsound(_ba.getsound('swish')) # If it exists, dismiss it; otherwise make a new one. if app.ui.party_window is not None and app.ui.party_window() is not None: app.ui.party_window().close() else: app.ui.party_window = weakref.ref(PartyWindow(origin=origin))
def on_player_leave(self, sessionplayer: ba.SessionPlayer) -> None: """Called when a previously-accepted ba.SessionPlayer leaves.""" if sessionplayer not in self.sessionplayers: 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 is still in the lobby; simply remove them. 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.sessionteam assert sessionteam is not None _ba.screenmessage( Lstr(resource='playerLeftText', subs=[('${PLAYER}', sessionplayer.getname(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.activityplayer 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: self._remove_player_team(sessionteam, activity) # Now remove them from the session list. self.sessionplayers.remove(sessionplayer)
def _request_storage_permission() -> bool: """If needed, requests storage permission from the user (& return true).""" from ba._language import Lstr from ba._enums import Permission if not _ba.have_permission(Permission.STORAGE): _ba.playsound(_ba.getsound('error')) _ba.screenmessage(Lstr(resource='storagePermissionAccessText'), color=(1, 0, 0)) _ba.timer(1.0, lambda: _ba.request_permission(Permission.STORAGE)) return True return False
def _tournament_time_limit_tick(self) -> None: from ba._gameutils import animate assert self._tournament_time_limit is not None self._tournament_time_limit -= 1 if self._tournament_time_limit <= 10: if self._tournament_time_limit == 10: assert self._tournament_time_limit_title_text is not None assert self._tournament_time_limit_title_text.node assert self._tournament_time_limit_text is not None assert self._tournament_time_limit_text.node self._tournament_time_limit_title_text.node.scale = 1.0 self._tournament_time_limit_text.node.scale = 1.3 self._tournament_time_limit_title_text.node.position = (80, 85) self._tournament_time_limit_text.node.position = (80, 60) cnode = _ba.newnode( 'combine', owner=self._tournament_time_limit_text.node, attrs={'size': 4}) cnode.connectattr('output', self._tournament_time_limit_title_text.node, 'color') cnode.connectattr('output', self._tournament_time_limit_text.node, 'color') animate(cnode, 'input0', {0: 1, 0.15: 1}, loop=True) animate(cnode, 'input1', {0: 1, 0.15: 0.5}, loop=True) animate(cnode, 'input2', {0: 0.1, 0.15: 0.0}, loop=True) cnode.input3 = 1.0 _ba.playsound(_ba.getsound('tick')) if self._tournament_time_limit <= 0: self._tournament_time_limit_timer = None self.end_game() tval = Lstr(resource='tournamentTimeExpiredText', fallback_resource='timeExpiredText') node = _ba.newnode('text', attrs={ 'v_attach': 'top', 'h_attach': 'center', 'h_align': 'center', 'color': (1, 0.7, 0, 1), 'position': (0, -200), 'scale': 1.6, 'text': tval }) _ba.playsound(_ba.getsound('refWhistle')) animate(node, 'scale', {0: 0.0, 0.1: 1.4, 0.15: 1.2}) # Normally we just connect this to time, but since this is a bit of a # funky setup we just update it manually once per second. assert self._tournament_time_limit_text_input is not None assert self._tournament_time_limit_text_input.node self._tournament_time_limit_text_input.node.time2 = ( self._tournament_time_limit * 1000)
def show_post_purchase_message(self) -> None: """(internal)""" from ba._language import Lstr from ba._generated.enums import TimeType cur_time = _ba.time(TimeType.REAL) if (self.last_post_purchase_message_time is None or cur_time - self.last_post_purchase_message_time > 3.0): self.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 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 _continue_choice(self, do_continue: bool) -> None: self._is_waiting_for_continue = False if self.has_ended(): return with _ba.Context(self): if do_continue: _ba.playsound(_ba.getsound('shieldUp')) _ba.playsound(_ba.getsound('cashRegister')) _ba.add_transaction({ 'type': 'CONTINUE', 'cost': self._continue_cost }) _ba.run_transactions() self._continue_cost = ( self._continue_cost * self._continue_cost_mult + self._continue_cost_offset) self.on_continue() else: self.end_game()
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 handlemessage(self, msg: Any) -> Any: """Standard generic message handler.""" if isinstance(msg, ChangeMessage): self._handle_repeat_message_attack() # 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 _apply(name2: Lstr, score2: int, showpoints2: bool, color2: tuple[float, float, float, float], scale2: float, sound2: Optional[ba.Sound]) -> None: from bastd.actor.popuptext import PopupText # Only award this if they're still alive and we can get # a current position for them. our_pos: Optional[ba.Vec3] = None if self._sessionplayer: if self._sessionplayer.activityplayer is not None: try: our_pos = self._sessionplayer.activityplayer.position except NotFoundError: pass if our_pos is None: return # Jitter position a bit since these often come in clusters. our_pos = _ba.Vec3(our_pos[0] + (random.random() - 0.5) * 2.0, our_pos[1] + (random.random() - 0.5) * 2.0, our_pos[2] + (random.random() - 0.5) * 2.0) activity = self.getactivity() if activity is not None: PopupText(Lstr( value=(('+' + str(score2) + ' ') if showpoints2 else '') + '${N}', subs=[('${N}', name2)]), color=color2, scale=scale2, position=our_pos).autoretain() if sound2: _ba.playsound(sound2) self.score += score2 self.accumscore += score2 # Inform a running game of the score. if score2 != 0 and activity is not None: activity.handlemessage(PlayerScoredMessage(score=score2))
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 _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_ready_msg(self, ready: bool) -> None: force_team_switch = False # Team auto-balance kicks us to another team if we try to # join the team with the most players. if not self._ready: if _ba.app.config.get('Auto Balance Teams', False): lobby = self.lobby sessionteams = lobby.sessionteams if len(sessionteams) > 1: # First, calc how many players are on each team # ..we need to count both active players and # choosers that have been marked as ready. team_player_counts = {} for sessionteam in sessionteams: team_player_counts[sessionteam.id] = len( sessionteam.players) for chooser in lobby.choosers: if chooser.ready: team_player_counts[chooser.sessionteam.id] += 1 largest_team_size = max(team_player_counts.values()) smallest_team_size = (min(team_player_counts.values())) # Force switch if we're on the biggest sessionteam # and there's a smaller one available. if (largest_team_size != smallest_team_size and team_player_counts[self.sessionteam.id] >= largest_team_size): force_team_switch = True # Either force switch teams, or actually for realsies do the set-ready. if force_team_switch: _ba.playsound(self._errorsound) self.handlemessage(ChangeMessage('team', 1)) else: _ba.playsound(self._punchsound) self._set_ready(ready)
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 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 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 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 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 play_gong_sound() -> None: _ba.playsound(_ba.getsound('gong'))
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 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 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()))
def _show_tip(self) -> None: # pylint: disable=too-many-locals from ba._gameutils import animate, GameTip from ba._enums import SpecialChar # If there's any tips left on the list, display one. if self.tips: tip = self.tips.pop(random.randrange(len(self.tips))) tip_title = Lstr(value='${A}:', subs=[('${A}', Lstr(resource='tipText'))]) icon: Optional[ba.Texture] = None sound: Optional[ba.Sound] = None if isinstance(tip, GameTip): icon = tip.icon sound = tip.sound tip = tip.text assert isinstance(tip, str) # Do a few substitutions. tip_lstr = Lstr(translate=('tips', tip), subs=[('${PICKUP}', _ba.charstr(SpecialChar.TOP_BUTTON))]) base_position = (75, 50) tip_scale = 0.8 tip_title_scale = 1.2 vrmode = _ba.app.vr_mode t_offs = -350.0 tnode = _ba.newnode('text', attrs={ 'text': tip_lstr, 'scale': tip_scale, 'maxwidth': 900, 'position': (base_position[0] + t_offs, base_position[1]), 'h_align': 'left', 'vr_depth': 300, 'shadow': 1.0 if vrmode else 0.5, 'flatness': 1.0 if vrmode else 0.5, 'v_align': 'center', 'v_attach': 'bottom' }) t2pos = (base_position[0] + t_offs - (20 if icon is None else 82), base_position[1] + 2) t2node = _ba.newnode('text', owner=tnode, attrs={ 'text': tip_title, 'scale': tip_title_scale, 'position': t2pos, 'h_align': 'right', 'vr_depth': 300, 'shadow': 1.0 if vrmode else 0.5, 'flatness': 1.0 if vrmode else 0.5, 'maxwidth': 140, 'v_align': 'center', 'v_attach': 'bottom' }) if icon is not None: ipos = (base_position[0] + t_offs - 40, base_position[1] + 1) img = _ba.newnode('image', attrs={ 'texture': icon, 'position': ipos, 'scale': (50, 50), 'opacity': 1.0, 'vr_depth': 315, 'color': (1, 1, 1), 'absolute_scale': True, 'attach': 'bottomCenter' }) animate(img, 'opacity', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0}) _ba.timer(5.0, img.delete) if sound is not None: _ba.playsound(sound) combine = _ba.newnode('combine', owner=tnode, attrs={ 'input0': 1.0, 'input1': 0.8, 'input2': 1.0, 'size': 4 }) combine.connectattr('output', tnode, 'color') combine.connectattr('output', t2node, 'color') animate(combine, 'input3', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0}) _ba.timer(5.0, tnode.delete)
def error_message() -> None: from ba._language import Lstr _ba.playsound(_ba.getsound('error')) _ba.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))