def spawn_player_spaz(self, player: PlayerType, position: Sequence[float] = (0, 0, 0), angle: float = None) -> PlayerSpaz: """Create and wire up a ba.PlayerSpaz for the provided ba.Player.""" # pylint: disable=too-many-locals # pylint: disable=cyclic-import from ba import _math from ba._gameutils import animate from ba._coopsession import CoopSession from bastd.actor.playerspaz import PlayerSpaz 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 = PlayerSpaz(color=color, highlight=highlight, character=player.character, player=player) player.actor = spaz assert spaz.node # 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 show_damage_count(damage: str, position: Sequence[float], direction: Sequence[float]) -> None: """Pop up a damage count at a position in space. Category: Gameplay Functions """ lifespan = 1.0 app = _ba.app # FIXME: Should never vary game elements based on local config. # (connected clients may have differing configs so they won't # get the intended results). do_big = app.ui.uiscale is UIScale.SMALL or app.vr_mode txtnode = _ba.newnode('text', attrs={ 'text': damage, 'in_world': True, 'h_align': 'center', 'flatness': 1.0, 'shadow': 1.0 if do_big else 0.7, 'color': (1, 0.25, 0.25, 1), 'scale': 0.015 if do_big else 0.01 }) # Translate upward. tcombine = _ba.newnode('combine', owner=txtnode, attrs={'size': 3}) tcombine.connectattr('output', txtnode, 'position') v_vals = [] pval = 0.0 vval = 0.07 count = 6 for i in range(count): v_vals.append((float(i) / count, pval)) pval += vval vval *= 0.5 p_start = position[0] p_dir = direction[0] animate(tcombine, 'input0', {i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals}) p_start = position[1] p_dir = direction[1] animate(tcombine, 'input1', {i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals}) p_start = position[2] p_dir = direction[2] animate(tcombine, 'input2', {i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals}) animate(txtnode, 'opacity', {0.7 * lifespan: 1.0, lifespan: 0.0}) _ba.timer(lifespan, txtnode.delete)
def print_corrupt_file_error() -> None: """Print an error if a corrupt file is found.""" from ba._general import Call from ba._generated.enums import TimeType _ba.timer(2.0, lambda: _ba.screenmessage( _ba.app.lang.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 snowfall(self) -> None: if hasattr(self, '_snowfall'): delattr(self, '_snowfall') if getattr(self, 'map', None): bounds = self.map.get_def_bound_box('map_bounds') def emits() -> None: for i in range(int(bounds[3] * bounds[5])): def _emit() -> None: _ba.emitfx( position=( random.uniform(bounds[0], bounds[3]), random.uniform(bounds[4] * 1.15, bounds[4] * 1.45), random.uniform(bounds[2], bounds[5]) ), velocity=(0, 0, 0), scale=random.uniform(0.5, 0.8), count=random.randint(7, 18), spread=random.uniform(0.05, 0.1), chunk_type='spark' ) _ba.timer(random.uniform(0.02, 0.05) * (i + 1), _emit) setattr(self, '_snowfall', _ba.timer(0.5, emits, repeat=True))
def _test(self) -> None: """For testing achievement animations.""" from ba._generated.enums import TimeType def testcall1() -> None: self.achievements[0].announce_completion() self.achievements[1].announce_completion() self.achievements[2].announce_completion() def testcall2() -> None: self.achievements[3].announce_completion() self.achievements[4].announce_completion() self.achievements[5].announce_completion() _ba.timer(3.0, testcall1, timetype=TimeType.BASE) _ba.timer(7.0, testcall2, timetype=TimeType.BASE)
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 emits() -> None: for i in range(int(bounds[3] * bounds[5])): def _emit() -> None: _ba.emitfx( position=( random.uniform(bounds[0], bounds[3]), random.uniform(bounds[4] * 1.15, bounds[4] * 1.45), random.uniform(bounds[2], bounds[5]) ), velocity=(0, 0, 0), scale=random.uniform(0.5, 0.8), count=random.randint(7, 18), spread=random.uniform(0.05, 0.1), chunk_type='spark' ) _ba.timer(random.uniform(0.02, 0.05) * (i + 1), _emit)
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 _execute_shutdown(self) -> None: from ba._lang import Lstr if self._executing_shutdown: return self._executing_shutdown = True timestrval = time.strftime('%c') if self._shutdown_reason is ShutdownReason.RESTARTING: _ba.screenmessage(Lstr(resource='internal.serverRestartingText'), color=(1, 0.5, 0.0)) print(f'{Clr.SBLU}Exiting for server-restart' f' at {timestrval}{Clr.RST}') else: _ba.screenmessage(Lstr(resource='internal.serverShuttingDownText'), color=(1, 0.5, 0.0)) print(f'{Clr.SBLU}Exiting for server-shutdown' f' at {timestrval}{Clr.RST}') with _ba.Context('ui'): _ba.timer(2.0, _ba.quit, timetype=TimeType.REAL)
def _handle_server_restarts(self) -> bool: """Handle automatic restarts/shutdowns in server mode. Returns True if an action was taken; otherwise default action should occur (starting next round, etc). """ # pylint: disable=cyclic-import # FIXME: Move server stuff to its own module. if self._allow_server_restart and _ba.app.server_config_dirty: from ba import _server from ba._lang import Lstr from ba._general import Call from ba._enums import TimeType if _ba.app.server_config.get('quit', False): if not self._kicked_off_server_shutdown: if _ba.app.server_config.get( 'quit_reason') == 'restarting': # FIXME: Should add a server-screen-message call # or something. _ba.chat_message( Lstr(resource='internal.serverRestartingText'). evaluate()) print(('Exiting for server-restart at ' + time.strftime('%c'))) else: print(('Exiting for server-shutdown at ' + time.strftime('%c'))) with _ba.Context('ui'): _ba.timer(2.0, _ba.quit, timetype=TimeType.REAL) self._kicked_off_server_shutdown = True return True else: if not self._kicked_off_server_restart: print(('Running updated server config at ' + time.strftime('%c'))) with _ba.Context('ui'): _ba.timer(1.0, Call(_ba.pushcall, _server.launch_server_session), timetype=TimeType.REAL) self._kicked_off_server_restart = True return True return False
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 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 verify_object_death(obj: object) -> None: """Warn if an object does not get freed within a short period. Category: General Utility Functions This can be handy to detect and prevent memory/resource leaks. """ try: ref = weakref.ref(obj) except Exception: print_exception('Unable to create weak-ref in verify_object_death') # Use a slight range for our checks so they don't all land at once # if we queue a lot of them. delay = random.uniform(2.0, 5.5) with _ba.Context('ui'): _ba.timer(delay, lambda: _verify_object_death(ref), timetype=TimeType.REAL)
def do_remove_in_game_ads_message(self) -> None: """(internal)""" from ba._lang import Lstr from ba._enums import TimeType # Print this message once every 10 minutes at most. tval = _ba.time(TimeType.REAL) if (self.last_in_game_ad_remove_message_show_time is None or (tval - self.last_in_game_ad_remove_message_show_time > 60 * 10)): self.last_in_game_ad_remove_message_show_time = tval with _ba.Context('ui'): _ba.timer( 1.0, lambda: _ba.screenmessage(Lstr( resource='removeInGameAdsText', subs=[('${PRO}', Lstr(resource='store.bombSquadProNameText')), ('${APP_NAME}', Lstr(resource='titleText'))]), color=(1, 1, 0)), timetype=TimeType.REAL)
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 set_main_menu_window(self, window: ba.Widget) -> None: """Set the current 'main' window, replacing any existing.""" existing = self._main_menu_window from ba._generated.enums import TimeType from inspect import currentframe, getframeinfo # Let's grab the location where we were called from to report # if we have to force-kill the existing window (which normally # should not happen). frameline = None try: frame = currentframe() if frame is not None: frame = frame.f_back if frame is not None: frameinfo = getframeinfo(frame) frameline = f'{frameinfo.filename} {frameinfo.lineno}' except Exception: from ba._error import print_exception print_exception('Error calcing line for set_main_menu_window') # With our legacy main-menu system, the caller is responsible for # clearing out the old main menu window when assigning the new. # However there are corner cases where that doesn't happen and we get # old windows stuck under the new main one. So let's guard against # that. However, we can't simply delete the existing main window when # a new one is assigned because the user may transition the old out # *after* the assignment. Sigh. So, as a happy medium, let's check in # on the old after a short bit of time and kill it if its still alive. # That will be a bit ugly on screen but at least should un-break # things. def _delay_kill() -> None: import time if existing: print(f'Killing old main_menu_window' f' when called at: {frameline} t={time.time():.3f}') existing.delete() _ba.timer(1.0, _delay_kill, timetype=TimeType.REAL) self._main_menu_window = window
def _execute_shutdown(self) -> None: from ba._lang import Lstr if self._executing_shutdown: return self._executing_shutdown = True timestrval = time.strftime('%c') if self._shutdown_reason is ShutdownReason.RESTARTING: # FIXME: Should add a server-screen-message call. # (so we could send this an an Lstr) _ba.chat_message( Lstr(resource='internal.serverRestartingText').evaluate()) print(f'{Clr.SBLU}Exiting for server-restart' f' at {timestrval}{Clr.RST}') else: # FIXME: Should add a server-screen-message call. # (so we could send this an an Lstr) print(f'{Clr.SBLU}Exiting for server-shutdown' f' at {timestrval}{Clr.RST}') _ba.chat_message( Lstr(resource='internal.serverShuttingDownText').evaluate()) with _ba.Context('ui'): _ba.timer(2.0, _ba.quit, timetype=TimeType.REAL)
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_app_launch(self) -> None: """Runs after the app finishes bootstrapping. (internal)""" # pylint: disable=too-many-locals # pylint: disable=cyclic-import from ba import _apputils from ba import _appconfig from ba import _achievement from ba import _map from ba import _campaign from bastd import appdelegate from bastd import maps as stdmaps from bastd.actor import spazappearance from ba._enums import TimeType cfg = self.config self.delegate = appdelegate.AppDelegate() self.ui.on_app_launch() spazappearance.register_appearances() _campaign.init_campaigns() # FIXME: This should not be hard-coded. for maptype in [ stdmaps.HockeyStadium, stdmaps.FootballStadium, stdmaps.Bridgit, stdmaps.BigG, stdmaps.Roundabout, stdmaps.MonkeyFace, stdmaps.ZigZag, stdmaps.ThePad, stdmaps.DoomShroom, stdmaps.LakeFrigid, stdmaps.TipTop, stdmaps.CragCastle, stdmaps.TowerD, stdmaps.HappyThoughts, stdmaps.StepRightUp, stdmaps.Courtyard, stdmaps.Rampage ]: _map.register_map(maptype) # Non-test, non-debug builds should generally be blessed; warn if not. # (so I don't accidentally release a build that can't play tourneys) if (not self.debug_build and not self.test_build and not _ba.is_blessed()): _ba.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0)) # If there's a leftover log file, attempt to upload it to the # master-server and/or get rid of it. _apputils.handle_leftover_log_file() # Only do this stuff if our config file is healthy so we don't # overwrite a broken one or whatnot and wipe out data. if not self.config_file_healthy: if self.platform in ('mac', 'linux', 'windows'): from bastd.ui import configerror configerror.ConfigErrorWindow() return # For now on other systems we just overwrite the bum config. # At this point settings are already set; lets just commit them # to disk. _appconfig.commit_app_config(force=True) self.music.on_app_launch() launch_count = cfg.get('launchCount', 0) launch_count += 1 # So we know how many times we've run the game at various # version milestones. for key in ('lc14173', 'lc14292'): cfg.setdefault(key, launch_count) # Debugging - make note if we're using the local test server so we # don't accidentally leave it on in a release. # FIXME - should move this to the native layer. server_addr = _ba.get_master_server_address() if 'localhost' in server_addr: _ba.timer(2.0, lambda: _ba.screenmessage( 'Note: using local server', (1, 1, 0), log=True, ), timetype=TimeType.REAL) elif 'test' in server_addr: _ba.timer(2.0, lambda: _ba.screenmessage( 'Note: using test server-module', (1, 1, 0), log=True, ), timetype=TimeType.REAL) cfg['launchCount'] = launch_count cfg.commit() # Run a test in a few seconds to see if we should pop up an existing # pending special offer. def check_special_offer() -> None: from bastd.ui.specialoffer import show_offer config = self.config if ('pendingSpecialOffer' in config and _ba.get_public_login_id() == config['pendingSpecialOffer']['a']): self.special_offer = config['pendingSpecialOffer']['o'] show_offer() if not self.headless_mode: _ba.timer(3.0, check_special_offer, timetype=TimeType.REAL) self.meta.on_app_launch() self.accounts.on_app_launch() self.plugins.on_app_launch() self.state = self.State.RUNNING
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 set_activity(self, activity: ba.Activity) -> None: """Assign a new current ba.Activity for the session. Note that this will not change the current context to the new Activity's. Code must be run in the new activity's methods (on_transition_in, etc) to get it. (so you can't do session.set_activity(foo) and then ba.newnode() to add a node to foo) """ # pylint: disable=too-many-statements # pylint: disable=too-many-branches from ba import _error from ba._gameutils import sharedobj from ba._enums import TimeType # Sanity test: make sure this doesn't get called recursively. if self._in_set_activity: raise Exception( 'Session.set_activity() cannot be called recursively.') if activity.session is not _ba.getsession(): raise Exception("Provided Activity's Session is not current.") # Quietly ignore this if the whole session is going down. if self._ending: return if activity is self._activity_retained: _error.print_error('activity set to already-current activity') return if self._next_activity is not None: raise Exception('Activity switch already in progress (to ' + str(self._next_activity) + ')') self._in_set_activity = True prev_activity = self._activity_retained if prev_activity is not None: with _ba.Context(prev_activity): gprev = sharedobj('globals') else: gprev = None with _ba.Context(activity): # 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 = activity.use_fixed_vr_overlay glb.allow_kick_idle_players = activity.allow_kick_idle_players if activity.inherits_slow_motion and gprev is not None: glb.slow_motion = gprev.slow_motion else: glb.slow_motion = activity.slow_motion if activity.inherits_music and gprev is not None: glb.music_continuous = True # Prevent restarting same music. glb.music = gprev.music glb.music_count += 1 if activity.inherits_camera_vr_offset and gprev is not None: glb.vr_camera_offset = gprev.vr_camera_offset if activity.inherits_vr_overlay_center and gprev is not None: glb.vr_overlay_center = gprev.vr_overlay_center glb.vr_overlay_center_enabled = gprev.vr_overlay_center_enabled # If they want to inherit tint from the previous activity. if activity.inherits_tint and gprev is not None: glb.tint = gprev.tint glb.vignette_outer = gprev.vignette_outer glb.vignette_inner = gprev.vignette_inner # Let the activity do its thing. activity.start_transition_in() self._next_activity = activity # If we have a current activity, tell it it's transitioning out; # the next one will become current once this one dies. if prev_activity is not None: # pylint: disable=protected-access prev_activity._transitioning_out = True # pylint: enable=protected-access # Activity will be None until the next one begins. with _ba.Context(prev_activity): prev_activity.on_transition_out() # Setting this to None should free up the old activity to die, # which will call begin_next_activity. # We can still access our old activity through # self._activity_weak() to keep it up to date on player # joins/departures/etc until it dies. self._activity_retained = None # There's no existing activity; lets just go ahead with the begin call. else: self.begin_next_activity() # Tell the C layer that this new activity is now 'foregrounded'. # This means that its globals node controls global stuff and stuff # like console operations, keyboard shortcuts, etc will run in it. # pylint: disable=protected-access # noinspection PyProtectedMember activity._activity_data.make_foreground() # pylint: enable=protected-access # We want to call _destroy() for the previous activity once it should # tear itself down, clear out any self-refs, etc. If the new activity # has a transition-time, set it up to be called after that passes; # otherwise call it immediately. After this call the activity should # have no refs left to it and should die (which will trigger the next # activity to run). if prev_activity is not None: if activity.transition_time > 0.0: # FIXME: We should tweak the activity to not allow # node-creation/etc when we call _destroy (or after). with _ba.Context('ui'): # pylint: disable=protected-access # noinspection PyProtectedMember _ba.timer(activity.transition_time, prev_activity._destroy, timetype=TimeType.REAL) # Just run immediately. else: # noinspection PyProtectedMember prev_activity._destroy() # pylint: disable=protected-access self._in_set_activity = False
def submit_kill(self, showpoints: bool = True) -> None: """Submit a kill for this player entry.""" # FIXME Clean this up. # pylint: disable=too-many-statements from ba._lang import Lstr from ba._general import Call self._multi_kill_count += 1 stats = self._stats() assert stats if self._multi_kill_count == 1: score = 0 name = None delay = 0.0 color = (0.0, 0.0, 0.0, 1.0) scale = 1.0 sound = None elif self._multi_kill_count == 2: score = 20 name = Lstr(resource='twoKillText') color = (0.1, 1.0, 0.0, 1) scale = 1.0 delay = 0.0 sound = stats.orchestrahitsound1 elif self._multi_kill_count == 3: score = 40 name = Lstr(resource='threeKillText') color = (1.0, 0.7, 0.0, 1) scale = 1.1 delay = 0.3 sound = stats.orchestrahitsound2 elif self._multi_kill_count == 4: score = 60 name = Lstr(resource='fourKillText') color = (1.0, 1.0, 0.0, 1) scale = 1.2 delay = 0.6 sound = stats.orchestrahitsound3 elif self._multi_kill_count == 5: score = 80 name = Lstr(resource='fiveKillText') color = (1.0, 0.5, 0.0, 1) scale = 1.3 delay = 0.9 sound = stats.orchestrahitsound4 else: score = 100 name = Lstr(resource='multiKillText', subs=[('${COUNT}', str(self._multi_kill_count))]) color = (1.0, 0.5, 0.0, 1) scale = 1.3 delay = 1.0 sound = stats.orchestrahitsound4 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[Sequence[float]] = None if self._player is not None: if self._player.gameplayer is not None: if self._player.gameplayer.node: our_pos = self._player.gameplayer.node.position if our_pos is None: return # Jitter position a bit since these often come in clusters. our_pos = (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)) if name is not None: _ba.timer( 0.3 + delay, Call(_apply, name, score, showpoints, color, scale, sound)) # Keep the tally rollin'... # set a timer for a bit in the future. self._multi_kill_timer = _ba.Timer(1.0, self._end_multi_kill)
def do_print() -> None: _ba.timer(1.0, Call(_ba.screenmessage, Lstr(resource='usingItunesText'), (0, 1, 0)), timetype=TimeType.REAL)
def on_app_launch(self) -> None: """Runs after the app finishes bootstrapping. (internal)""" # FIXME: Break this up. # pylint: disable=too-many-statements # pylint: disable=too-many-locals # pylint: disable=cyclic-import from ba import _apputils from ba import _appconfig from ba.ui import UIController, ui_upkeep from ba import _achievement from ba import _map from ba import _meta from ba import _campaign from bastd import appdelegate from bastd import maps as stdmaps from bastd.actor import spazappearance from ba._enums import TimeType cfg = self.config self.delegate = appdelegate.AppDelegate() self.uicontroller = UIController() _achievement.init_achievements() spazappearance.register_appearances() _campaign.init_campaigns() # FIXME: This should not be hard-coded. for maptype in [ stdmaps.HockeyStadium, stdmaps.FootballStadium, stdmaps.Bridgit, stdmaps.BigG, stdmaps.Roundabout, stdmaps.MonkeyFace, stdmaps.ZigZag, stdmaps.ThePad, stdmaps.DoomShroom, stdmaps.LakeFrigid, stdmaps.TipTop, stdmaps.CragCastle, stdmaps.TowerD, stdmaps.HappyThoughts, stdmaps.StepRightUp, stdmaps.Courtyard, stdmaps.Rampage ]: _map.register_map(maptype) if self.debug_build: _apputils.suppress_debug_reports() # IMPORTANT - if tweaking UI stuff, you need to make sure it behaves # for small, medium, and large UI modes. (doesn't run off screen, etc). # Set these to 1 to test with different sizes. Generally small is used # on phones, medium is used on tablets, and large is on desktops or # large tablets. # Kick off our periodic UI upkeep. # FIXME: Can probably kill this if we do immediate UI death checks. self.uiupkeeptimer = _ba.Timer(2.6543, ui_upkeep, timetype=TimeType.REAL, repeat=True) if bool(False): # force-test small UI self.small_ui = True self.med_ui = False with _ba.Context('ui'): _ba.pushcall(lambda: _ba.screenmessage( 'FORCING SMALL UI FOR TESTING', color=(1, 0, 1), log=True)) if bool(False): # force-test medium UI self.small_ui = False self.med_ui = True with _ba.Context('ui'): _ba.pushcall(lambda: _ba.screenmessage( 'FORCING MEDIUM UI FOR TESTING', color= (1, 0, 1), log=True)) if bool(False): # force-test large UI self.small_ui = False self.med_ui = False with _ba.Context('ui'): _ba.pushcall(lambda: _ba.screenmessage( 'FORCING LARGE UI FOR TESTING', color=(1, 0, 1), log=True)) # If there's a leftover log file, attempt to upload # it to the server and/or get rid of it. _apputils.handle_leftover_log_file() try: _apputils.handle_leftover_log_file() except Exception: from ba import _error _error.print_exception('Error handling leftover log file') # Notify the user if we're using custom system scripts. # FIXME: This no longer works since sys-scripts is an absolute path; # need to just add a proper call to query this. # if env['python_directory_ba'] != 'data/scripts': # ba.screenmessage("Using custom system scripts...", # color=(0, 1, 0)) # Only do this stuff if our config file is healthy so we don't # overwrite a broken one or whatnot and wipe out data. if not self.config_file_healthy: if self.platform in ('mac', 'linux', 'windows'): from bastd.ui import configerror configerror.ConfigErrorWindow() return # For now on other systems we just overwrite the bum config. # At this point settings are already set; lets just commit them # to disk. _appconfig.commit_app_config(force=True) self.music.on_app_launch() launch_count = cfg.get('launchCount', 0) launch_count += 1 # So we know how many times we've run the game at various # version milestones. for key in ('lc14173', 'lc14292'): cfg.setdefault(key, launch_count) # Debugging - make note if we're using the local test server so we # don't accidentally leave it on in a release. # FIXME - move this to native layer. server_addr = _ba.get_master_server_address() if 'localhost' in server_addr: _ba.timer(2.0, lambda: _ba.screenmessage('Note: using local server', (1, 1, 0), log=True), timetype=TimeType.REAL) elif 'test' in server_addr: _ba.timer( 2.0, lambda: _ba.screenmessage('Note: using test server-module', (1, 1, 0), log=True), timetype=TimeType.REAL) cfg['launchCount'] = launch_count cfg.commit() # Run a test in a few seconds to see if we should pop up an existing # pending special offer. def check_special_offer() -> None: from bastd.ui import specialoffer config = self.config if ('pendingSpecialOffer' in config and _ba.get_public_login_id() == config['pendingSpecialOffer']['a']): self.special_offer = config['pendingSpecialOffer']['o'] specialoffer.show_offer() if not self.headless_build: _ba.timer(3.0, check_special_offer, timetype=TimeType.REAL) # Start scanning for things exposed via ba_meta. _meta.start_scan() # Auto-sign-in to a local account in a moment if we're set to. def do_auto_sign_in() -> None: if self.headless_build: _ba.sign_in('Local') elif cfg.get('Auto Account State') == 'Local': _ba.sign_in('Local') _ba.pushcall(do_auto_sign_in) self.ran_on_app_launch = True
def on_begin(self) -> None: super().on_begin() # Die almost immediately. _ba.timer(0.1, self.end)
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 _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 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 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 setactivity(self, activity: ba.Activity) -> None: """Assign a new current ba.Activity for the session. Note that this will not change the current context to the new Activity's. Code must be run in the new activity's methods (on_transition_in, etc) to get it. (so you can't do session.setactivity(foo) and then ba.newnode() to add a node to foo) """ from ba._enums import TimeType # Make sure we don't get called recursively. _rlock = self._SetActivityScopedLock(self) if activity.session is not _ba.getsession(): raise RuntimeError("Provided Activity's Session is not current.") # Quietly ignore this if the whole session is going down. if self._ending: return if activity is self._activity_retained: print_error('Activity set to already-current activity.') return if self._next_activity is not None: raise RuntimeError('Activity switch already in progress (to ' + str(self._next_activity) + ')') prev_activity = self._activity_retained prev_globals = (prev_activity.globalsnode if prev_activity is not None else None) # Let the activity do its thing. activity.transition_in(prev_globals) self._next_activity = activity # If we have a current activity, tell it it's transitioning out; # the next one will become current once this one dies. if prev_activity is not None: prev_activity.transition_out() # Setting this to None should free up the old activity to die, # which will call begin_next_activity. # We can still access our old activity through # self._activity_weak() to keep it up to date on player # joins/departures/etc until it dies. self._activity_retained = None # There's no existing activity; lets just go ahead with the begin call. else: self.begin_next_activity() # We want to call destroy() for the previous activity once it should # tear itself down, clear out any self-refs, etc. After this call # the activity should have no refs left to it and should die (which # will trigger the next activity to run). if prev_activity is not None: with _ba.Context('ui'): _ba.timer(max(0.0, activity.transition_time), prev_activity.expire, timetype=TimeType.REAL) self._in_set_activity = False