def _standard_time_limit_tick(self) -> None: from ba._gameutils import animate assert self._standard_time_limit_time is not None self._standard_time_limit_time -= 1 if self._standard_time_limit_time <= 10: if self._standard_time_limit_time == 10: assert self._standard_time_limit_text is not None assert self._standard_time_limit_text.node self._standard_time_limit_text.node.scale = 1.3 self._standard_time_limit_text.node.position = (-30, -45) cnode = _ba.newnode('combine', owner=self._standard_time_limit_text.node, attrs={'size': 4}) cnode.connectattr('output', self._standard_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._standard_time_limit_time <= 0: self._standard_time_limit_timer = None self.end_game() node = _ba.newnode('text', attrs={ 'v_attach': 'top', 'h_attach': 'center', 'h_align': 'center', 'color': (1, 0.7, 0, 1), 'position': (0, -90), 'scale': 1.2, 'text': Lstr(resource='timeExpiredText') }) _ba.playsound(_ba.getsound('refWhistle')) animate(node, 'scale', {0.0: 0.0, 0.1: 1.4, 0.15: 1.2})
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_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 _play_internal_music(self, musictype: Optional[MusicType]) -> None: # Stop any existing music-player playback. if self._music_player is not None: self._music_player.stop() # Stop any existing internal music. if self._music_node: self._music_node.delete() self._music_node = None # Start up new internal music. if musictype is not None: entry = ASSET_SOUNDTRACK_ENTRIES.get(musictype) if entry is None: print(f"Unknown music: '{musictype}'") entry = ASSET_SOUNDTRACK_ENTRIES[MusicType.FLAG_CATCHER] self._music_node = _ba.newnode(type='sound', attrs={ 'sound': _ba.getsound(entry.assetname), 'positional': False, 'music': True, 'volume': entry.volume * 5.0, 'loop': entry.loop })
def pause(self) -> None: """Pause the game due to a user request or menu popping up. If there's a foreground host-activity that says it's pausable, tell it to pause ..we now no longer pause if there are connected clients. """ activity: Optional[ba.Activity] = _ba.get_foreground_host_activity() if (activity is not None and activity.allow_pausing and not _ba.have_connected_clients()): from ba import _gameutils from ba._language import Lstr from ba._nodeactor import NodeActor # FIXME: Shouldn't be touching scene stuff here; # should just pass the request on to the host-session. with _ba.Context(activity): globs = activity.globalsnode if not globs.paused: _ba.playsound(_ba.getsound('refWhistle')) globs.paused = True # FIXME: This should not be an attr on Actor. activity.paused_text = NodeActor( _ba.newnode('text', attrs={ 'text': Lstr(resource='pausedByHostText'), 'client_only': True, 'flatness': 1.0, 'h_align': 'center' }))
def postinit(self, sessionplayer: ba.SessionPlayer) -> None: """Wire up a newly created player. (internal) """ from ba._nodeactor import NodeActor # Sanity check; if a dataclass is created that inherits from us, # it will define an equality operator by default which will break # internal game logic. So complain loudly if we find one. if type(self).__eq__ is not object.__eq__: raise RuntimeError( f'Player class {type(self)} defines an equality' f' operator (__eq__) which will break internal' f' logic. Please remove it.\n' f'For dataclasses you can do "dataclass(eq=False)"' f' in the class decorator.') self.actor = None self.character = '' self._nodeactor: Optional[ba.NodeActor] = None self._sessionplayer = sessionplayer self.character = sessionplayer.character self.color = sessionplayer.color self.highlight = sessionplayer.highlight self._team = cast(TeamType, sessionplayer.sessionteam.activityteam) assert self._team is not None self._customdata = {} self._expired = False self._postinited = True node = _ba.newnode('player', attrs={'playerID': sessionplayer.id}) self._nodeactor = NodeActor(node) sessionplayer.setnode(node)
def _play_internal_music(musictype: Optional[MusicType]) -> None: app = _ba.app # Stop any existing music-player playback. if app.music_player is not None: app.music_player.stop() # Stop any existing internal music. if app.music: app.music.delete() app.music = None # Start up new internal music. if musictype is not None: # Filenames/volume/loop for our built-in music. musicinfos: Dict[MusicType, Tuple[str, float, bool]] = { MusicType.MENU: ('menuMusic', 5.0, True), MusicType.VICTORY: ('victoryMusic', 6.0, False), MusicType.CHAR_SELECT: ('charSelectMusic', 2.0, True), MusicType.RUN_AWAY: ('runAwayMusic', 6.0, True), MusicType.ONSLAUGHT: ('runAwayMusic', 6.0, True), MusicType.KEEP_AWAY: ('runAwayMusic', 6.0, True), MusicType.RACE: ('runAwayMusic', 6.0, True), MusicType.EPIC_RACE: ('slowEpicMusic', 6.0, True), MusicType.SCORES: ('scoresEpicMusic', 3.0, False), MusicType.GRAND_ROMP: ('grandRompMusic', 6.0, True), MusicType.TO_THE_DEATH: ('toTheDeathMusic', 6.0, True), MusicType.CHOSEN_ONE: ('survivalMusic', 4.0, True), MusicType.FORWARD_MARCH: ('forwardMarchMusic', 4.0, True), MusicType.FLAG_CATCHER: ('flagCatcherMusic', 6.0, True), MusicType.SURVIVAL: ('survivalMusic', 4.0, True), MusicType.EPIC: ('slowEpicMusic', 6.0, True), MusicType.SPORTS: ('sportsMusic', 4.0, True), MusicType.HOCKEY: ('sportsMusic', 4.0, True), MusicType.FOOTBALL: ('sportsMusic', 4.0, True), MusicType.FLYING: ('flyingMusic', 4.0, True), MusicType.SCARY: ('scaryMusic', 4.0, True), MusicType.MARCHING: ('whenJohnnyComesMarchingHomeMusic', 4.0, True), } musicinfo = musicinfos.get(musictype) if musicinfo is None: print(f"Unknown music: '{musictype}'") filename = 'flagCatcherMusic' volume = 6.0 loop = True else: filename, volume, loop = musicinfo app.music = _ba.newnode(type='sound', attrs={ 'sound': _ba.getsound(filename), 'positional': False, 'music': True, 'volume': volume, 'loop': loop })
def create_player_node(self, player: ba.Player) -> ba.Node: """Create the 'player' node associated with the provided ba.Player.""" from ba._nodeactor import NodeActor with _ba.Context(self): node = _ba.newnode('player', attrs={'playerID': player.get_id()}) # FIXME: Should add a dedicated slot for this on ba.Player # instead of cluttering up their gamedata dict. player.gamedata['_playernode'] = NodeActor(node) return node
def __init__(self, lobby: ba.Lobby): from ba._nodeactor import NodeActor from ba._general import WeakCall self._state = 0 self._press_to_punch: Union[str, ba.Lstr] = ('C' if _ba.app.iircade_mode else _ba.charstr( SpecialChar.LEFT_BUTTON)) self._press_to_bomb: Union[str, ba.Lstr] = ('B' if _ba.app.iircade_mode else _ba.charstr( SpecialChar.RIGHT_BUTTON)) self._joinmsg = Lstr(resource='pressAnyButtonToJoinText') can_switch_teams = (len(lobby.sessionteams) > 1) # If we have a keyboard, grab keys for punch and pickup. # FIXME: This of course is only correct on the local device; # Should change this for net games. keyboard = _ba.getinputdevice('Keyboard', '#1', doraise=False) if keyboard is not None: self._update_for_keyboard(keyboard) flatness = 1.0 if _ba.app.vr_mode else 0.0 self._text = NodeActor( _ba.newnode('text', attrs={ 'position': (0, -40), 'h_attach': 'center', 'v_attach': 'top', 'h_align': 'center', 'color': (0.7, 0.7, 0.95, 1.0), 'flatness': flatness, 'text': self._joinmsg })) if _ba.app.demo_mode or _ba.app.arcade_mode: self._messages = [self._joinmsg] else: msg1 = Lstr(resource='pressToSelectProfileText', subs=[ ('${BUTTONS}', _ba.charstr(SpecialChar.UP_ARROW) + ' ' + _ba.charstr(SpecialChar.DOWN_ARROW)) ]) msg2 = Lstr(resource='pressToOverrideCharacterText', subs=[('${BUTTONS}', Lstr(resource='bombBoldText'))]) msg3 = Lstr(value='${A} < ${B} >', subs=[('${A}', msg2), ('${B}', self._press_to_bomb)]) self._messages = (([ Lstr( resource='pressToSelectTeamText', subs=[('${BUTTONS}', _ba.charstr(SpecialChar.LEFT_ARROW) + ' ' + _ba.charstr(SpecialChar.RIGHT_ARROW))], ) ] if can_switch_teams else []) + [msg1] + [msg3] + [self._joinmsg]) self._timer = _ba.Timer(4.0, WeakCall(self._update), repeat=True)
def transition_in(self, prev_globals: Optional[ba.Node]) -> None: """Called by Session to kick off transition-in. (internal) """ assert not self._has_transitioned_in self._has_transitioned_in = True # Set up the globals node based on our settings. with _ba.Context(self): glb = self._globalsnode = _ba.newnode('globals') # Now that it's going to be front and center, # set some global values based on what the activity wants. glb.use_fixed_vr_overlay = self.use_fixed_vr_overlay glb.allow_kick_idle_players = self.allow_kick_idle_players if self.inherits_slow_motion and prev_globals is not None: glb.slow_motion = prev_globals.slow_motion else: glb.slow_motion = self.slow_motion if self.inherits_music and prev_globals is not None: glb.music_continuous = True # Prevent restarting same music. glb.music = prev_globals.music glb.music_count += 1 if self.inherits_vr_camera_offset and prev_globals is not None: glb.vr_camera_offset = prev_globals.vr_camera_offset if self.inherits_vr_overlay_center and prev_globals is not None: glb.vr_overlay_center = prev_globals.vr_overlay_center glb.vr_overlay_center_enabled = ( prev_globals.vr_overlay_center_enabled) # If they want to inherit tint from the previous self. if self.inherits_tint and prev_globals is not None: glb.tint = prev_globals.tint glb.vignette_outer = prev_globals.vignette_outer glb.vignette_inner = prev_globals.vignette_inner # Start pruning our various things periodically. self._prune_dead_actors() self._prune_dead_actors_timer = _ba.Timer(5.17, self._prune_dead_actors, repeat=True) _ba.timer(13.3, self._prune_delay_deletes, repeat=True) # Also start our low-level scene running. self._activity_data.start() try: self.on_transition_in() except Exception: print_exception(f'Error in on_transition_in for {self}.') # Tell the C++ layer that this activity is the main one, so it uses # settings from our globals, directs various events to us, etc. self._activity_data.make_foreground()
def 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 top_message(self, text): node = _ba.newnode('text', attrs={ 'text': text, 'flatness': 1.0, 'h_align': 'center', 'v_attach':'top', 'scale':0.7, 'position':(0,-70), 'color':(1,1,1) })
def left_watermark(self, text): node = _ba.newnode('text', attrs={ 'text': text, 'flatness': 1.0, 'h_align': 'left', 'v_attach':'bottom', 'h_attach':'left', 'scale':0.7, 'position':(25,67), 'color':(0.7,0.7,0.7) })
def nextGame(self,text): node = _ba.newnode('text', attrs={ 'text':"Next : "+text, 'flatness':1.0, 'h_align':'right', 'v_attach':'bottom', 'h_attach':'right', 'scale':0.7, 'position':(-25,18), 'color':(0.5,0.5,0.5) })
def fade_to_red(self) -> None: """Fade the screen to red; (such as when the good guys have lost).""" from ba import _gameutils c_existing = self.globalsnode.tint cnode = _ba.newnode('combine', attrs={ 'input0': c_existing[0], 'input1': c_existing[1], 'input2': c_existing[2], 'size': 3 }) _gameutils.animate(cnode, 'input1', {0: c_existing[1], 2.0: 0}) _gameutils.animate(cnode, 'input2', {0: c_existing[2], 2.0: 0}) cnode.connectattr('output', self.globalsnode, 'tint')
def setup_standard_time_limit(self, duration: float) -> None: """ Create a standard game time-limit given the provided duration in seconds. This will be displayed at the top of the screen. If the time-limit expires, end_game() will be called. """ from ba._nodeactor import NodeActor if duration <= 0.0: return self._standard_time_limit_time = int(duration) self._standard_time_limit_timer = _ba.Timer( 1.0, WeakCall(self._standard_time_limit_tick), repeat=True) self._standard_time_limit_text = NodeActor( _ba.newnode('text', attrs={ 'v_attach': 'top', 'h_attach': 'center', 'h_align': 'left', 'color': (1.0, 1.0, 1.0, 0.5), 'position': (-25, -30), 'flatness': 1.0, 'scale': 0.9 })) self._standard_time_limit_text_input = NodeActor( _ba.newnode('timedisplay', attrs={ 'time2': duration * 1000, 'timemin': 0 })) self.globalsnode.connectattr('time', self._standard_time_limit_text_input.node, 'time1') assert self._standard_time_limit_text_input.node assert self._standard_time_limit_text.node self._standard_time_limit_text_input.node.connectattr( 'output', self._standard_time_limit_text.node, 'text')
def highlights_(self): if setti["textonmap"]['center highlights']["randomColor"]: color=((0+random.random()*1.0),(0+random.random()*1.0),(0+random.random()*1.0)) else: color=tuple(setti["textonmap"]["center highlights"]["color"]) node = _ba.newnode('text', attrs={ 'text': self.highlights[self.index], 'flatness': 1.0, 'h_align': 'center', 'v_attach':'bottom', 'scale':1, 'position':(0,138), 'color':color }) self.delt = ba.timer(7,node.delete) self.index = int((self.index+1)%len(self.highlights))
def _update_life_warning(self) -> None: # Beep continuously if anyone is close to death. should_beep = False for player in self.players: if player.is_alive(): # FIXME: Should abstract this instead of # reading hitpoints directly. if getattr(player.actor, 'hitpoints', 999) < 200: should_beep = True break if should_beep and self._life_warning_beep is None: from ba._nodeactor import NodeActor self._life_warning_beep = NodeActor( _ba.newnode('sound', attrs={ 'sound': self._warn_beeps_sound, 'positional': False, 'loop': True })) if self._life_warning_beep is not None and not should_beep: self._life_warning_beep = None
def _show_standard_scores_to_beat_ui(self, scores: List[Dict[str, Any]]) -> None: from efro.util import asserttype from ba._gameutils import timestring, animate from ba._nodeactor import NodeActor from ba._enums import TimeFormat display_type = self.get_score_type() if scores is not None: # Sort by originating date so that the most recent is first. scores.sort(reverse=True, key=lambda s: asserttype(s['time'], int)) # Now make a display for the most recent challenge. for score in scores: if score['type'] == 'score_challenge': tval = (score['player'] + ': ' + timestring( int(score['value']) * 10, timeformat=TimeFormat.MILLISECONDS).evaluate() if display_type == 'time' else str(score['value'])) hattach = 'center' if display_type == 'time' else 'left' halign = 'center' if display_type == 'time' else 'left' pos = (20, -70) if display_type == 'time' else (20, -130) txt = NodeActor( _ba.newnode('text', attrs={ 'v_attach': 'top', 'h_attach': hattach, 'h_align': halign, 'color': (0.7, 0.4, 1, 1), 'shadow': 0.5, 'flatness': 1.0, 'position': pos, 'scale': 0.6, 'text': tval })).autoretain() assert txt.node is not None animate(txt.node, 'scale', {1.0: 0.0, 1.1: 0.7, 1.2: 0.6}) break
def postinit(self, sessionplayer: ba.SessionPlayer) -> None: """Wire up a newly created player. (internal) """ from ba._nodeactor import NodeActor # Sanity check; if a dataclass is created that inherits from us, # it will define an equality operator by default which will break # internal game logic. So complain loudly if we find one. if type(self).__eq__ is not object.__eq__: raise RuntimeError( f'Player class {type(self)} defines an equality' f' operator (__eq__) which will break internal' f' logic. Please remove it.\n' f'For dataclasses you can do "dataclass(eq=False)"' f' in the class decorator.') self.actor = None self.character = '' self._nodeactor: Optional[ba.NodeActor] = None self._sessionplayer = sessionplayer self.character = sessionplayer.character self.color = sessionplayer.color self.highlight = sessionplayer.highlight self.team = sessionplayer.team.gameteam # type: ignore assert self.team is not None self.sessiondata = sessionplayer.sessiondata self.gamedata = sessionplayer.gamedata # Create our player node in the current activity. # Note: do we want to save a few cycles here by managing our player # node manually instead of wrapping it in a NodeActor? node = _ba.newnode('player', attrs={'playerID': sessionplayer.id}) self._nodeactor = NodeActor(node) sessionplayer.set_node(node)
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 _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_scoreboard_info(self) -> None: """Create the game info display. This is the thing in the top left corner showing the name and short description of the game. """ # pylint: disable=too-many-locals from ba._freeforallsession import FreeForAllSession from ba._gameutils import animate from ba._nodeactor import NodeActor sb_name = self.get_instance_scoreboard_display_string() # The description can be either a string or a sequence with args # to swap in post-translation. sb_desc_in = self.get_instance_description_short() sb_desc_l: Sequence if isinstance(sb_desc_in, str): sb_desc_l = [sb_desc_in] # handle simple string case else: sb_desc_l = sb_desc_in if not isinstance(sb_desc_l[0], str): raise TypeError('Invalid format for instance description.') is_empty = (sb_desc_l[0] == '') subs = [] for i in range(len(sb_desc_l) - 1): subs.append(('${ARG' + str(i + 1) + '}', str(sb_desc_l[i + 1]))) translation = Lstr(translate=('gameDescriptions', sb_desc_l[0]), subs=subs) sb_desc = translation vrmode = _ba.app.vr_mode yval = -34 if is_empty else -20 yval -= 16 sbpos = ((15, yval) if isinstance(self.session, FreeForAllSession) else (15, yval)) self._game_scoreboard_name_text = NodeActor( _ba.newnode('text', attrs={ 'text': sb_name, 'maxwidth': 300, 'position': sbpos, 'h_attach': 'left', 'vr_depth': 10, 'v_attach': 'top', 'v_align': 'bottom', 'color': (1.0, 1.0, 1.0, 1.0), 'shadow': 1.0 if vrmode else 0.6, 'flatness': 1.0 if vrmode else 0.5, 'scale': 1.1 })) assert self._game_scoreboard_name_text.node animate(self._game_scoreboard_name_text.node, 'opacity', { 0: 0.0, 1.0: 1.0 }) descpos = (((17, -44 + 10) if isinstance(self.session, FreeForAllSession) else (17, -44 + 10))) self._game_scoreboard_description_text = NodeActor( _ba.newnode('text', attrs={ 'text': sb_desc, 'maxwidth': 480, 'position': descpos, 'scale': 0.7, 'h_attach': 'left', 'v_attach': 'top', 'v_align': 'top', 'shadow': 1.0 if vrmode else 0.7, 'flatness': 1.0 if vrmode else 0.8, 'color': (1, 1, 1, 1) if vrmode else (0.9, 0.9, 0.9, 1.0) })) assert self._game_scoreboard_description_text.node animate(self._game_scoreboard_description_text.node, 'opacity', { 0: 0.0, 1.0: 1.0 })
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 _setup_tournament_time_limit(self, duration: float) -> None: """ Create a tournament game time-limit given the provided duration in seconds. This will be displayed at the top of the screen. If the time-limit expires, end_game() will be called. """ from ba._nodeactor import NodeActor from ba._enums import TimeType if duration <= 0.0: return self._tournament_time_limit = int(duration) # We want this timer to match the server's time as close as possible, # so lets go with base-time. Theoretically we should do real-time but # then we have to mess with contexts and whatnot since its currently # not available in activity contexts. :-/ self._tournament_time_limit_timer = _ba.Timer( 1.0, WeakCall(self._tournament_time_limit_tick), repeat=True, timetype=TimeType.BASE) self._tournament_time_limit_title_text = NodeActor( _ba.newnode('text', attrs={ 'v_attach': 'bottom', 'h_attach': 'left', 'h_align': 'center', 'v_align': 'center', 'vr_depth': 300, 'maxwidth': 100, 'color': (1.0, 1.0, 1.0, 0.5), 'position': (60, 50), 'flatness': 1.0, 'scale': 0.5, 'text': Lstr(resource='tournamentText') })) self._tournament_time_limit_text = NodeActor( _ba.newnode('text', attrs={ 'v_attach': 'bottom', 'h_attach': 'left', 'h_align': 'center', 'v_align': 'center', 'vr_depth': 300, 'maxwidth': 100, 'color': (1.0, 1.0, 1.0, 0.5), 'position': (60, 30), 'flatness': 1.0, 'scale': 0.9 })) self._tournament_time_limit_text_input = NodeActor( _ba.newnode('timedisplay', attrs={ 'timemin': 0, 'time2': self._tournament_time_limit * 1000 })) assert self._tournament_time_limit_text.node assert self._tournament_time_limit_text_input.node self._tournament_time_limit_text_input.node.connectattr( 'output', self._tournament_time_limit_text.node, 'text')
def cameraflash(duration: float = 999.0) -> None: """Create a strobing camera flash effect. Category: Gameplay Functions (as seen when a team wins a game) Duration is in seconds. """ # pylint: disable=too-many-locals import random from ba._nodeactor import NodeActor x_spread = 10 y_spread = 5 positions = [[-x_spread, -y_spread], [0, -y_spread], [0, y_spread], [x_spread, -y_spread], [x_spread, y_spread], [-x_spread, y_spread]] times = [0, 2700, 1000, 1800, 500, 1400] # Store this on the current activity so we only have one at a time. # FIXME: Need a type safe way to do this. activity = _ba.getactivity() activity.camera_flash_data = [] # type: ignore for i in range(6): light = NodeActor( _ba.newnode('light', attrs={ 'position': (positions[i][0], 0, positions[i][1]), 'radius': 1.0, 'lights_volumes': False, 'height_attenuated': False, 'color': (0.2, 0.2, 0.8) })) sval = 1.87 iscale = 1.3 tcombine = _ba.newnode('combine', owner=light.node, attrs={ 'size': 3, 'input0': positions[i][0], 'input1': 0, 'input2': positions[i][1] }) assert light.node tcombine.connectattr('output', light.node, 'position') xval = positions[i][0] yval = positions[i][1] spd = 0.5 + random.random() spd2 = 0.5 + random.random() animate(tcombine, 'input0', { 0.0: xval + 0, 0.069 * spd: xval + 10.0, 0.143 * spd: xval - 10.0, 0.201 * spd: xval + 0 }, loop=True) animate(tcombine, 'input2', { 0.0: yval + 0, 0.15 * spd2: yval + 10.0, 0.287 * spd2: yval - 10.0, 0.398 * spd2: yval + 0 }, loop=True) animate(light.node, 'intensity', { 0.0: 0, 0.02 * sval: 0, 0.05 * sval: 0.8 * iscale, 0.08 * sval: 0, 0.1 * sval: 0 }, loop=True, offset=times[i]) _ba.timer((times[i] + random.randint(1, int(duration)) * 40 * sval), light.node.delete, timeformat=TimeFormat.MILLISECONDS) activity.camera_flash_data.append(light) # type: ignore
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 __init__(self, depsets: Sequence[ba.DependencySet], team_names: Sequence[str] = None, team_colors: Sequence[Sequence[float]] = None, min_players: int = 1, max_players: int = 8): """Instantiate a session. depsets should be a sequence of successfully resolved ba.DependencySet instances; one for each ba.Activity the session may potentially run. """ # pylint: disable=too-many-statements # pylint: disable=too-many-locals # pylint: disable=cyclic-import from ba._lobby import Lobby from ba._stats import Stats from ba._gameactivity import GameActivity from ba._activity import Activity from ba._team import SessionTeam from ba._error import DependencyError from ba._dependency import Dependency, AssetPackage from efro.util import empty_weakref # First off, resolve all dependency-sets we were passed. # If things are missing, we'll try to gather them into a single # missing-deps exception if possible to give the caller a clean # path to download missing stuff and try again. missing_asset_packages: Set[str] = set() for depset in depsets: try: depset.resolve() except DependencyError as exc: # Gather/report missing assets only; barf on anything else. if all(issubclass(d.cls, AssetPackage) for d in exc.deps): for dep in exc.deps: assert isinstance(dep.config, str) missing_asset_packages.add(dep.config) else: missing_info = [(d.cls, d.config) for d in exc.deps] raise RuntimeError( f'Missing non-asset dependencies: {missing_info}' ) from exc # Throw a combined exception if we found anything missing. if missing_asset_packages: raise DependencyError([ Dependency(AssetPackage, set_id) for set_id in missing_asset_packages ]) # Ok; looks like our dependencies check out. # Now give the engine a list of asset-set-ids to pass along to clients. required_asset_packages: Set[str] = set() for depset in depsets: required_asset_packages.update(depset.get_asset_package_ids()) # print('Would set host-session asset-reqs to:', # required_asset_packages) # Init our C++ layer data. self._sessiondata = _ba.register_session(self) # Should remove this if possible. self.tournament_id: Optional[str] = None self.sessionteams = [] self.sessionplayers = [] self.min_players = min_players self.max_players = max_players self.customdata = {} self._in_set_activity = False self._next_team_id = 0 self._activity_retained: Optional[ba.Activity] = None self._launch_end_session_activity_time: Optional[float] = None self._activity_end_timer: Optional[ba.Timer] = None self._activity_weak = empty_weakref(Activity) self._next_activity: Optional[ba.Activity] = None self._wants_to_end = False self._ending = False self._activity_should_end_immediately = False self._activity_should_end_immediately_results: ( Optional[ba.GameResults]) = None self._activity_should_end_immediately_delay = 0.0 # Create static teams if we're using them. if self.use_teams: assert team_names is not None assert team_colors is not None for i, color in enumerate(team_colors): team = SessionTeam(team_id=self._next_team_id, name=GameActivity.get_team_display_string( team_names[i]), color=color) self.sessionteams.append(team) self._next_team_id += 1 try: with _ba.Context(self): self.on_team_join(team) except Exception: print_exception(f'Error in on_team_join for {self}.') self.lobby = Lobby() self.stats = Stats() # Instantiate our session globals node which will apply its settings. self._sessionglobalsnode = _ba.newnode('sessionglobals')
def __init__(self, vpos: float, sessionplayer: _ba.SessionPlayer, lobby: 'Lobby') -> None: self._deek_sound = _ba.getsound('deek') self._click_sound = _ba.getsound('click01') self._punchsound = _ba.getsound('punch01') self._swish_sound = _ba.getsound('punchSwish') self._errorsound = _ba.getsound('error') self._mask_texture = _ba.gettexture('characterIconMask') self._vpos = vpos self._lobby = weakref.ref(lobby) self._sessionplayer = sessionplayer self._inited = False self._dead = False self._text_node: Optional[ba.Node] = None self._profilename = '' self._profilenames: List[str] = [] self._ready: bool = False self._character_names: List[str] = [] self._last_change: Sequence[Union[float, int]] = (0, 0) self._profiles: Dict[str, Dict[str, Any]] = {} app = _ba.app # Load available player profiles either from the local config or # from the remote device. self.reload_profiles() # Note: this is just our local index out of available teams; *not* # the team-id! self._selected_team_index: int = self.lobby.next_add_team # Store a persistent random character index and colors; we'll use this # for the '_random' profile. Let's use their input_device id to seed # it. This will give a persistent character for them between games # and will distribute characters nicely if everyone is random. self._random_color, self._random_highlight = ( get_player_profile_colors(None)) # To calc our random character we pick a random one out of our # unlocked list and then locate that character's index in the full # list. char_index_offset = app.lobby_random_char_index_offset self._random_character_index = ( (sessionplayer.inputdevice.id + char_index_offset) % len(self._character_names)) # Attempt to set an initial profile based on what was used previously # for this input-device, etc. self._profileindex = self._select_initial_profile() self._profilename = self._profilenames[self._profileindex] self._text_node = _ba.newnode('text', delegate=self, attrs={ 'position': (-100, self._vpos), 'maxwidth': 160, 'shadow': 0.5, 'vr_depth': -20, 'h_align': 'left', 'v_align': 'center', 'v_attach': 'top' }) animate(self._text_node, 'scale', {0: 0, 0.1: 1.0}) self.icon = _ba.newnode('image', owner=self._text_node, attrs={ 'position': (-130, self._vpos + 20), 'mask_texture': self._mask_texture, 'vr_depth': -10, 'attach': 'topCenter' }) animate_array(self.icon, 'scale', 2, {0: (0, 0), 0.1: (45, 45)}) # Set our initial name to '<choosing player>' in case anyone asks. self._sessionplayer.setname( Lstr(resource='choosingPlayerText').evaluate(), real=False) # Init these to our rando but they should get switched to the # selected profile (if any) right after. self._character_index = self._random_character_index self._color = self._random_color self._highlight = self._random_highlight self.update_from_profile() self.update_position() self._inited = True self._set_ready(False)