def add_actor_weak_ref(self, actor: ba.Actor) -> None: """Add a weak-reference to a ba.Actor to the ba.Activity. (called by the ba.Actor base class) """ from ba import _actor as bsactor if not isinstance(actor, bsactor.Actor): raise TypeError('non-actor passed to add_actor_weak_ref') # Make sure our pruning is happening... assert (not self.has_transitioned_in() or _ba.time() - self._last_prune_dead_actors_time < 10.0) self._actor_weak_refs.append(weakref.ref(actor))
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 add_actor_weak_ref(self, actor: ba.Actor) -> None: """Add a weak-reference to a ba.Actor to the ba.Activity. (called by the ba.Actor base class) """ from ba import _actor as bsactor if not isinstance(actor, bsactor.Actor): raise TypeError('non-actor passed to add_actor_weak_ref') if (self.has_transitioned_in() and _ba.time() - self._last_prune_dead_actors_time > 10.0): print_error('it looks like nodes/actors are ' 'not being pruned in your activity;' ' did you call Activity.on_transition_in()' ' from your subclass?; ' + str(self) + ' (loc. b)') self._actor_weak_refs.append(weakref.ref(actor))
def __init__(self, settings: Dict[str, Any]): super().__init__(settings) self.transition_time = 0.5 self.inherits_tint = True self.inherits_camera_vr_offset = True self.use_fixed_vr_overlay = True self.default_music: Optional[MusicType] = MusicType.SCORES self._birth_time = _ba.time() self._min_view_time = 5.0 self._allow_server_restart = False self._background: Optional[ba.Actor] = None self._tips_text: Optional[ba.Actor] = None self._kicked_off_server_shutdown = False self._kicked_off_server_restart = False self._default_show_tips = True self._custom_continue_message: Optional[ba.Lstr] = None
def retain_actor(self, actor: ba.Actor) -> None: """Add a strong-reference to a ba.Actor to this Activity. The reference will be lazily released once ba.Actor.exists() returns False for the Actor. The ba.Actor.autoretain() method is a convenient way to access this same functionality. """ from ba import _actor as bsactor if not isinstance(actor, bsactor.Actor): raise TypeError('non-actor passed to retain_actor') # Make sure our pruning is happening... assert (not self.has_transitioned_in() or _ba.time() - self._last_prune_dead_actors_time < 10.0) self._actor_refs.append(actor)
def retain_actor(self, actor: ba.Actor) -> None: """Add a strong-reference to a ba.Actor to this Activity. The reference will be lazily released once ba.Actor.exists() returns False for the Actor. The ba.Actor.autoretain() method is a convenient way to access this same functionality. """ from ba import _actor as bsactor if not isinstance(actor, bsactor.Actor): raise TypeError('non-actor passed to retain_actor') if (self.has_transitioned_in() and _ba.time() - self._last_prune_dead_actors_time > 10.0): print_error('it looks like nodes/actors are not' ' being pruned in your activity;' ' did you call Activity.on_transition_in()' ' from your subclass?; ' + str(self) + ' (loc. a)') self._actor_refs.append(actor)
def _launch_end_session_activity(self) -> None: """(internal)""" from ba._activitytypes import EndSessionActivity from ba._enums import TimeType with _ba.Context(self): curtime = _ba.time(TimeType.REAL) if self._ending: # Ignore repeats unless its been a while. assert self._launch_end_session_activity_time is not None since_last = (curtime - self._launch_end_session_activity_time) if since_last < 30.0: return print_error( '_launch_end_session_activity called twice (since_last=' + str(since_last) + ')') self._launch_end_session_activity_time = curtime self.setactivity(_ba.newactivity(EndSessionActivity)) self._wants_to_end = False self._ending = True # Prevent further actions.
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 __init__(self, settings: dict): """Creates an Activity in the current ba.Session. The activity will not be actually run until ba.Session.setactivity is called. 'settings' should be a dict of key/value pairs specific to the activity. Activities should preload as much of their media/etc as possible in their constructor, but none of it should actually be used until they are transitioned in. """ super().__init__() # Create our internal engine data. self._activity_data = _ba.register_activity(self) assert isinstance(settings, dict) assert _ba.getactivity() is self self._globalsnode: Optional[ba.Node] = None # Player/Team types should have been specified as type args; # grab those. self._playertype: type[PlayerType] self._teamtype: type[TeamType] self._setup_player_and_team_types() # FIXME: Relocate or remove the need for this stuff. self.paused_text: Optional[ba.Actor] = None self._session = weakref.ref(_ba.getsession()) # Preloaded data for actors, maps, etc; indexed by type. self.preloads: dict[type, Any] = {} # Hopefully can eventually kill this; activities should # validate/store whatever settings they need at init time # (in a more type-safe way). self.settings_raw = settings self._has_transitioned_in = False self._has_begun = False self._has_ended = False self._activity_death_check_timer: Optional[ba.Timer] = None self._expired = False self._delay_delete_players: list[PlayerType] = [] self._delay_delete_teams: list[TeamType] = [] self._players_that_left: list[weakref.ref[PlayerType]] = [] self._teams_that_left: list[weakref.ref[TeamType]] = [] self._transitioning_out = False # A handy place to put most actors; this list is pruned of dead # actors regularly and these actors are insta-killed as the activity # is dying. self._actor_refs: list[ba.Actor] = [] self._actor_weak_refs: list[weakref.ref[ba.Actor]] = [] self._last_prune_dead_actors_time = _ba.time() self._prune_dead_actors_timer: Optional[ba.Timer] = None self.teams = [] self.players = [] self.lobby = None self._stats: Optional[ba.Stats] = None self._customdata: Optional[dict] = {}
def __init__(self, settings: Dict[str, Any]): """Creates an activity in the current ba.Session. The activity will not be actually run until ba.Session.set_activity() is called. 'settings' should be a dict of key/value pairs specific to the activity. Activities should preload as much of their media/etc as possible in their constructor, but none of it should actually be used until they are transitioned in. """ super().__init__() # FIXME: Relocate this stuff. self.sharedobjs: Dict[str, Any] = {} self.paused_text: Optional[ba.Actor] = None self.spaz_respawn_icons_right: Dict[int, RespawnIcon] # Create our internal engine data. self._activity_data = _ba.register_activity(self) session = _ba.getsession() if session is None: raise Exception("No current session") self._session = weakref.ref(session) # Preloaded data for actors, maps, etc; indexed by type. self.preloads: Dict[Type, Any] = {} if not isinstance(settings, dict): raise Exception("expected dict for settings") if _ba.getactivity(doraise=False) is not self: raise Exception('invalid context state') self.settings = settings self._has_transitioned_in = False self._has_begun = False self._has_ended = False self._should_end_immediately = False self._should_end_immediately_results: ( Optional[ba.TeamGameResults]) = None self._should_end_immediately_delay = 0.0 self._called_activity_on_transition_in = False self._called_activity_on_begin = False self._activity_death_check_timer: Optional[ba.Timer] = None self._expired = False # Whether to print every time a player dies. This can be pertinent # in games such as Death-Match but can be annoying in games where it # doesn't matter. self.announce_player_deaths = False # Joining activities are for waiting for initial player joins. # They are treated slightly differently than regular activities, # mainly in that all players are passed to the activity at once # instead of as each joins. self.is_joining_activity = False # Whether game-time should still progress when in menus/etc. self.allow_pausing = False # Whether idle players can potentially be kicked (should not happen in # menus/etc). self.allow_kick_idle_players = True # In vr mode, this determines whether overlay nodes (text, images, etc) # are created at a fixed position in space or one that moves based on # the current map. Generally this should be on for games and off for # transitions/score-screens/etc. that persist between maps. self.use_fixed_vr_overlay = False # If True, runs in slow motion and turns down sound pitch. self.slow_motion = False # Set this to True to inherit slow motion setting from previous # activity (useful for transitions to avoid hitches). self.inherits_slow_motion = False # Set this to True to keep playing the music from the previous activity # (without even restarting it). self.inherits_music = False # Set this to true to inherit VR camera offsets from the previous # activity (useful for preventing sporadic camera movement # during transitions). self.inherits_camera_vr_offset = False # Set this to true to inherit (non-fixed) VR overlay positioning from # the previous activity (useful for prevent sporadic overlay jostling # during transitions). self.inherits_vr_overlay_center = False # Set this to true to inherit screen tint/vignette colors from the # previous activity (useful to prevent sudden color changes during # transitions). self.inherits_tint = False # If the activity fades or transitions in, it should set the length of # time here so that previous activities will be kept alive for that # long (avoiding 'holes' in the screen) # This value is given in real-time seconds. self.transition_time = 0.0 # Is it ok to show an ad after this activity ends before showing # the next activity? self.can_show_ad_on_death = False # This gets set once another activity has begun transitioning in but # before this one is killed. The on_transition_out() method is also # called at this time. Make sure to not assign player inputs, # change music, or anything else with global implications once this # happens. self._transitioning_out = False # A handy place to put most actors; this list is pruned of dead # actors regularly and these actors are insta-killed as the activity # is dying. self._actor_refs: List[ba.Actor] = [] self._actor_weak_refs: List[ReferenceType[ba.Actor]] = [] self._last_dead_object_prune_time = _ba.time() # This stuff gets filled in just before on_begin() is called. self.teams = [] self.players = [] self._stats: Optional[ba.Stats] = None self.lobby = None self._prune_dead_objects_timer: Optional[ba.Timer] = None
def get_available_sale_time(tab: str) -> Optional[int]: """(internal)""" # pylint: disable=too-many-branches # pylint: disable=too-many-nested-blocks # pylint: disable=too-many-locals try: import datetime from ba._generated.enums import TimeType, TimeFormat app = _ba.app sale_times: List[Optional[int]] = [] # Calc time for our pro sale (old special case). if tab == 'extras': config = app.config if app.accounts.have_pro(): return None # If we haven't calced/loaded start times yet. if app.pro_sale_start_time is None: # If we've got a time-remaining in our config, start there. if 'PSTR' in config: app.pro_sale_start_time = int( _ba.time(TimeType.REAL, TimeFormat.MILLISECONDS)) app.pro_sale_start_val = config['PSTR'] else: # We start the timer once we get the duration from # the server. start_duration = _ba.get_account_misc_read_val( 'proSaleDurationMinutes', None) if start_duration is not None: app.pro_sale_start_time = int( _ba.time(TimeType.REAL, TimeFormat.MILLISECONDS)) app.pro_sale_start_val = (60000 * start_duration) # If we haven't heard from the server yet, no sale.. else: return None assert app.pro_sale_start_val is not None val: Optional[int] = max( 0, app.pro_sale_start_val - (_ba.time(TimeType.REAL, TimeFormat.MILLISECONDS) - app.pro_sale_start_time)) # Keep the value in the config up to date. I suppose we should # write the config occasionally but it should happen often enough # for other reasons. config['PSTR'] = val if val == 0: val = None sale_times.append(val) # Now look for sales in this tab. sales_raw = _ba.get_account_misc_read_val('sales', {}) store_layout = get_store_layout() for section in store_layout[tab]: for item in section['items']: if item in sales_raw: if not _ba.get_purchased(item): to_end = ((datetime.datetime.utcfromtimestamp( sales_raw[item]['e']) - datetime.datetime.utcnow()).total_seconds()) if to_end > 0: sale_times.append(int(to_end * 1000)) # Return the smallest time I guess? sale_times_int = [t for t in sale_times if isinstance(t, int)] return min(sale_times_int) if sale_times_int else None except Exception: from ba import _error _error.print_exception('error calcing sale time') return None
def __init__(self, settings: Dict[str, Any]): """Creates an Activity in the current ba.Session. The activity will not be actually run until ba.Session.set_activity() is called. 'settings' should be a dict of key/value pairs specific to the activity. Activities should preload as much of their media/etc as possible in their constructor, but none of it should actually be used until they are transitioned in. """ super().__init__() # Create our internal engine data. self._activity_data = _ba.register_activity(self) # Player/Team types should have been specified as type args; # grab those. self._playertype: Type[PlayerType] self._teamtype: Type[TeamType] self._setup_player_and_team_types() # FIXME: Relocate or remove the need for this stuff. self.sharedobjs: Dict[str, Any] = {} self.paused_text: Optional[ba.Actor] = None self.spaz_respawn_icons_right: Dict[int, RespawnIcon] session = _ba.getsession() if session is None: raise RuntimeError('No current session') self._session = weakref.ref(session) # Preloaded data for actors, maps, etc; indexed by type. self.preloads: Dict[Type, Any] = {} if not isinstance(settings, dict): raise TypeError('expected dict for settings') if _ba.getactivity(doraise=False) is not self: raise Exception('invalid context state') # Hopefully can eventually kill this; activities should # validate/store whatever settings they need at init time # (in a more type-safe way). self.settings_raw = settings self._has_transitioned_in = False self._has_begun = False self._has_ended = False self._should_end_immediately = False self._should_end_immediately_results: ( Optional[ba.TeamGameResults]) = None self._should_end_immediately_delay = 0.0 self._called_activity_on_transition_in = False self._called_activity_on_begin = False self._activity_death_check_timer: Optional[ba.Timer] = None self._expired = False # This gets set once another activity has begun transitioning in but # before this one is killed. The on_transition_out() method is also # called at this time. Make sure to not assign player inputs, # change music, or anything else with global implications once this # happens. self._transitioning_out = False # A handy place to put most actors; this list is pruned of dead # actors regularly and these actors are insta-killed as the activity # is dying. self._actor_refs: List[ba.Actor] = [] self._actor_weak_refs: List[ReferenceType[ba.Actor]] = [] self._last_prune_dead_actors_time = _ba.time() self._prune_dead_actors_timer: Optional[ba.Timer] = None # This stuff gets filled in just before on_begin() is called. self.teams = [] self.players = [] self.lobby = None self._stats: Optional[ba.Stats] = None
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 animate(node: ba.Node, attr: str, keys: dict[float, float], loop: bool = False, offset: float = 0, timetype: ba.TimeType = TimeType.SIM, timeformat: ba.TimeFormat = TimeFormat.SECONDS, suppress_format_warning: bool = False) -> ba.Node: """Animate values on a target ba.Node. Category: Gameplay Functions Creates an 'animcurve' node with the provided values and time as an input, connect it to the provided attribute, and set it to die with the target. Key values are provided as time:value dictionary pairs. Time values are relative to the current time. By default, times are specified in seconds, but timeformat can also be set to MILLISECONDS to recreate the old behavior (prior to ba 1.5) of taking milliseconds. Returns the animcurve node. """ if timetype is TimeType.SIM: driver = 'time' else: raise Exception('FIXME; only SIM timetype is supported currently.') items = list(keys.items()) items.sort() # Temp sanity check while we transition from milliseconds to seconds # based time values. if __debug__: if not suppress_format_warning: for item in items: _ba.time_format_check(timeformat, item[0]) curve = _ba.newnode('animcurve', owner=node, name='Driving ' + str(node) + ' \'' + attr + '\'') if timeformat is TimeFormat.SECONDS: mult = 1000 elif timeformat is TimeFormat.MILLISECONDS: mult = 1 else: raise ValueError(f'invalid timeformat value: {timeformat}') curve.times = [int(mult * time) for time, val in items] curve.offset = _ba.time(timeformat=TimeFormat.MILLISECONDS) + int( mult * offset) curve.values = [val for time, val in items] curve.loop = loop # If we're not looping, set a timer to kill this curve # after its done its job. # FIXME: Even if we are looping we should have a way to die once we # get disconnected. if not loop: _ba.timer(int(mult * items[-1][0]) + 1000, curve.delete, timeformat=TimeFormat.MILLISECONDS) # Do the connects last so all our attrs are in place when we push initial # values through. # We operate in either activities or sessions.. try: globalsnode = _ba.getactivity().globalsnode except ActivityNotFoundError: globalsnode = _ba.getsession().sessionglobalsnode globalsnode.connectattr(driver, curve, 'in') curve.connectattr('out', node, attr) return curve
def animate_array(node: ba.Node, attr: str, size: int, keys: dict[float, Sequence[float]], loop: bool = False, offset: float = 0, timetype: ba.TimeType = TimeType.SIM, timeformat: ba.TimeFormat = TimeFormat.SECONDS, suppress_format_warning: bool = False) -> None: """Animate an array of values on a target ba.Node. Category: Gameplay Functions Like ba.animate(), but operates on array attributes. """ # pylint: disable=too-many-locals combine = _ba.newnode('combine', owner=node, attrs={'size': size}) if timetype is TimeType.SIM: driver = 'time' else: raise Exception('FIXME: Only SIM timetype is supported currently.') items = list(keys.items()) items.sort() # Temp sanity check while we transition from milliseconds to seconds # based time values. if __debug__: if not suppress_format_warning: for item in items: # (PyCharm seems to think item is a float, not a tuple) _ba.time_format_check(timeformat, item[0]) if timeformat is TimeFormat.SECONDS: mult = 1000 elif timeformat is TimeFormat.MILLISECONDS: mult = 1 else: raise ValueError('invalid timeformat value: "' + str(timeformat) + '"') # We operate in either activities or sessions.. try: globalsnode = _ba.getactivity().globalsnode except ActivityNotFoundError: globalsnode = _ba.getsession().sessionglobalsnode for i in range(size): curve = _ba.newnode('animcurve', owner=node, name=('Driving ' + str(node) + ' \'' + attr + '\' member ' + str(i))) globalsnode.connectattr(driver, curve, 'in') curve.times = [int(mult * time) for time, val in items] curve.values = [val[i] for time, val in items] curve.offset = _ba.time(timeformat=TimeFormat.MILLISECONDS) + int( mult * offset) curve.loop = loop curve.connectattr('out', combine, 'input' + str(i)) # If we're not looping, set a timer to kill this # curve after its done its job. if not loop: # (PyCharm seems to think item is a float, not a tuple) _ba.timer(int(mult * items[-1][0]) + 1000, curve.delete, timeformat=TimeFormat.MILLISECONDS) combine.connectattr('output', node, attr) # If we're not looping, set a timer to kill the combine once # the job is done. # FIXME: Even if we are looping we should have a way to die # once we get disconnected. if not loop: # (PyCharm seems to think item is a float, not a tuple) _ba.timer(int(mult * items[-1][0]) + 1000, combine.delete, timeformat=TimeFormat.MILLISECONDS)
def call_after_ad(call: Callable[[], Any]) -> None: """Run a call after potentially showing an ad.""" # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=too-many-locals from ba._account import have_pro from ba._enums import TimeType import time app = _ba.app show = True # No ads without net-connections, etc. if not _ba.can_show_ad(): show = False if have_pro(): show = False # Pro disables interstitials. try: session = _ba.get_foreground_host_session() assert session is not None is_tournament = session.tournament_id is not None except Exception: is_tournament = False if is_tournament: show = False # Never show ads during tournaments. if show: interval: Optional[float] launch_count = app.config.get('launchCount', 0) # If we're seeing short ads we may want to space them differently. interval_mult = (_ba.get_account_misc_read_val( 'ads.shortIntervalMult', 1.0) if app.last_ad_was_short else 1.0) if app.ad_amt is None: if launch_count <= 1: app.ad_amt = _ba.get_account_misc_read_val( 'ads.startVal1', 0.99) else: app.ad_amt = _ba.get_account_misc_read_val( 'ads.startVal2', 1.0) interval = None else: # So far we're cleared to show; now calc our ad-show-threshold and # see if we should *actually* show (we reach our threshold faster # the longer we've been playing). base = 'ads' if _ba.has_video_ads() else 'ads2' min_lc = _ba.get_account_misc_read_val(base + '.minLC', 0.0) max_lc = _ba.get_account_misc_read_val(base + '.maxLC', 5.0) min_lc_scale = (_ba.get_account_misc_read_val( base + '.minLCScale', 0.25)) max_lc_scale = (_ba.get_account_misc_read_val( base + '.maxLCScale', 0.34)) min_lc_interval = (_ba.get_account_misc_read_val( base + '.minLCInterval', 360)) max_lc_interval = (_ba.get_account_misc_read_val( base + '.maxLCInterval', 300)) if launch_count < min_lc: lc_amt = 0.0 elif launch_count > max_lc: lc_amt = 1.0 else: lc_amt = ((float(launch_count) - min_lc) / (max_lc - min_lc)) incr = (1.0 - lc_amt) * min_lc_scale + lc_amt * max_lc_scale interval = ((1.0 - lc_amt) * min_lc_interval + lc_amt * max_lc_interval) app.ad_amt += incr assert app.ad_amt is not None if app.ad_amt >= 1.0: app.ad_amt = app.ad_amt % 1.0 app.attempted_first_ad = True # After we've reached the traditional show-threshold once, # try again whenever its been INTERVAL since our last successful show. elif (app.attempted_first_ad and (app.last_ad_completion_time is None or (interval is not None and _ba.time(TimeType.REAL) - app.last_ad_completion_time > (interval * interval_mult)))): # Reset our other counter too in this case. app.ad_amt = 0.0 else: show = False # If we're *still* cleared to show, actually tell the system to show. if show: # As a safety-check, set up an object that will run # the completion callback if we've returned and sat for 10 seconds # (in case some random ad network doesn't properly deliver its # completion callback). class _Payload: def __init__(self, pcall: Callable[[], Any]): self._call = pcall self._ran = False def run(self, fallback: bool = False) -> None: """Run the fallback call (and issues a warning about it).""" if not self._ran: if fallback: print(( 'ERROR: relying on fallback ad-callback! ' 'last network: ' + app.last_ad_network + ' (set ' + str(int(time.time() - app.last_ad_network_set_time)) + 's ago); purpose=' + app.last_ad_purpose)) _ba.pushcall(self._call) self._ran = True payload = _Payload(call) with _ba.Context('ui'): _ba.timer(5.0, lambda: payload.run(fallback=True), timetype=TimeType.REAL) show_ad('between_game', on_completion_call=payload.run) else: _ba.pushcall(call) # Just run the callback without the ad.
def _prune_dead_objects(self) -> None: self._actor_refs = [a for a in self._actor_refs if a] self._actor_weak_refs = [a for a in self._actor_weak_refs if a()] self._last_dead_object_prune_time = _ba.time()