def _show_winner(self, team: ba.SessionTeam) -> None: from bastd.actor.image import Image from bastd.actor.zoomtext import ZoomText if not self._is_ffa: offs_v = 0.0 ZoomText(team.name, position=(0, 97), color=team.color, scale=1.15, jitter=1.0, maxwidth=250).autoretain() else: offs_v = -80.0 if len(team.players) == 1: i = Image(team.players[0].get_icon(), position=(0, 143), scale=(100, 100)).autoretain() assert i.node ba.animate(i.node, 'opacity', {0.0: 0.0, 0.25: 1.0}) ZoomText(ba.Lstr( value=team.players[0].getname(full=True, icon=False)), position=(0, 97 + offs_v), color=team.color, scale=1.15, jitter=1.0, maxwidth=250).autoretain() s_extra = 1.0 if self._is_ffa else 1.0 # Some languages say "FOO WINS" differently for teams vs players. if isinstance(self.session, ba.FreeForAllSession): wins_resource = 'seriesWinLine1PlayerText' else: wins_resource = 'seriesWinLine1TeamText' wins_text = ba.Lstr(resource=wins_resource) # Temp - if these come up as the english default, fall-back to the # unified old form which is more likely to be translated. ZoomText(wins_text, position=(0, -10 + offs_v), color=team.color, scale=0.65 * s_extra, jitter=1.0, maxwidth=250).autoretain() ZoomText(ba.Lstr(resource='seriesWinLine2Text'), position=(0, -110 + offs_v), scale=1.0 * s_extra, color=team.color, jitter=1.0, maxwidth=250).autoretain()
def on_begin(self) -> None: # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-statements from bastd.actor.text import Text from bastd.actor.image import Image from ba.deprecated import get_resource ba.set_analytics_screen('FreeForAll Series Victory Screen' if self. _is_ffa else 'Teams Series Victory Screen') if ba.app.uiscale is ba.UIScale.LARGE: sval = ba.Lstr(resource='pressAnyKeyButtonPlayAgainText') else: sval = ba.Lstr(resource='pressAnyButtonPlayAgainText') self._show_up_next = False self._custom_continue_message = sval super().on_begin() winning_sessionteam = self.settings_raw['winner'] # Pause a moment before playing victory music. ba.timer(0.6, ba.WeakCall(self._play_victory_music)) ba.timer(4.4, ba.WeakCall(self._show_winner, self.settings_raw['winner'])) ba.timer(4.6, ba.Call(ba.playsound, self._score_display_sound)) # Score / Name / Player-record. player_entries: List[Tuple[int, str, ba.PlayerRecord]] = [] # Note: for ffa, exclude players who haven't entered the game yet. if self._is_ffa: for _pkey, prec in self.stats.get_records().items(): if prec.player.in_game: player_entries.append( (prec.player.sessionteam.customdata['score'], prec.getname(full=True), prec)) player_entries.sort(reverse=True, key=lambda x: x[0]) else: for _pkey, prec in self.stats.get_records().items(): player_entries.append((prec.score, prec.name_full, prec)) player_entries.sort(reverse=True, key=lambda x: x[0]) ts_height = 300.0 ts_h_offs = -390.0 tval = 6.4 t_incr = 0.12 always_use_first_to = get_resource('bestOfUseFirstToInstead') session = self.session if self._is_ffa: assert isinstance(session, ba.FreeForAllSession) txt = ba.Lstr( value='${A}:', subs=[('${A}', ba.Lstr(resource='firstToFinalText', subs=[('${COUNT}', str(session.get_ffa_series_length()))])) ]) else: assert isinstance(session, ba.MultiTeamSession) # Some languages may prefer to always show 'first to X' instead of # 'best of X'. # FIXME: This will affect all clients connected to us even if # they're not using this language. Should try to come up # with a wording that works everywhere. if always_use_first_to: txt = ba.Lstr( value='${A}:', subs=[ ('${A}', ba.Lstr(resource='firstToFinalText', subs=[ ('${COUNT}', str(session.get_series_length() / 2 + 1)) ])) ]) else: txt = ba.Lstr( value='${A}:', subs=[('${A}', ba.Lstr(resource='bestOfFinalText', subs=[('${COUNT}', str(session.get_series_length()))])) ]) Text(txt, v_align=Text.VAlign.CENTER, maxwidth=300, color=(0.5, 0.5, 0.5, 1.0), position=(0, 220), scale=1.2, transition=Text.Transition.IN_TOP_SLOW, h_align=Text.HAlign.CENTER, transition_delay=t_incr * 4).autoretain() win_score = (session.get_series_length() - 1) // 2 + 1 lose_score = 0 for team in self.teams: if team.sessionteam.customdata['score'] != win_score: lose_score = team.sessionteam.customdata['score'] if not self._is_ffa: Text(ba.Lstr(resource='gamesToText', subs=[('${WINCOUNT}', str(win_score)), ('${LOSECOUNT}', str(lose_score))]), color=(0.5, 0.5, 0.5, 1.0), maxwidth=160, v_align=Text.VAlign.CENTER, position=(0, -215), scale=1.8, transition=Text.Transition.IN_LEFT, h_align=Text.HAlign.CENTER, transition_delay=4.8 + t_incr * 4).autoretain() if self._is_ffa: v_extra = 120 else: v_extra = 0 mvp: Optional[ba.PlayerRecord] = None mvp_name: Optional[str] = None # Show game MVP. if not self._is_ffa: mvp, mvp_name = None, None for entry in player_entries: if entry[2].team == winning_sessionteam: mvp = entry[2] mvp_name = entry[1] break if mvp is not None: Text(ba.Lstr(resource='mostValuablePlayerText'), color=(0.5, 0.5, 0.5, 1.0), v_align=Text.VAlign.CENTER, maxwidth=300, position=(180, ts_height / 2 + 15), transition=Text.Transition.IN_LEFT, h_align=Text.HAlign.LEFT, transition_delay=tval).autoretain() tval += 4 * t_incr Image(mvp.get_icon(), position=(230, ts_height / 2 - 55 + 14 - 5), scale=(70, 70), transition=Image.Transition.IN_LEFT, transition_delay=tval).autoretain() assert mvp_name is not None Text(ba.Lstr(value=mvp_name), position=(280, ts_height / 2 - 55 + 15 - 5), h_align=Text.HAlign.LEFT, v_align=Text.VAlign.CENTER, maxwidth=170, scale=1.3, color=ba.safecolor(mvp.team.color + (1, )), transition=Text.Transition.IN_LEFT, transition_delay=tval).autoretain() tval += 4 * t_incr # Most violent. most_kills = 0 for entry in player_entries: if entry[2].kill_count >= most_kills: mvp = entry[2] mvp_name = entry[1] most_kills = entry[2].kill_count if mvp is not None: Text(ba.Lstr(resource='mostViolentPlayerText'), color=(0.5, 0.5, 0.5, 1.0), v_align=Text.VAlign.CENTER, maxwidth=300, position=(180, ts_height / 2 - 150 + v_extra + 15), transition=Text.Transition.IN_LEFT, h_align=Text.HAlign.LEFT, transition_delay=tval).autoretain() Text(ba.Lstr(value='(${A})', subs=[('${A}', ba.Lstr(resource='killsTallyText', subs=[('${COUNT}', str(most_kills))])) ]), position=(260, ts_height / 2 - 150 - 15 + v_extra), color=(0.3, 0.3, 0.3, 1.0), scale=0.6, h_align=Text.HAlign.LEFT, transition=Text.Transition.IN_LEFT, transition_delay=tval).autoretain() tval += 4 * t_incr Image(mvp.get_icon(), position=(233, ts_height / 2 - 150 - 30 - 46 + 25 + v_extra), scale=(50, 50), transition=Image.Transition.IN_LEFT, transition_delay=tval).autoretain() assert mvp_name is not None Text(ba.Lstr(value=mvp_name), position=(270, ts_height / 2 - 150 - 30 - 36 + v_extra + 15), h_align=Text.HAlign.LEFT, v_align=Text.VAlign.CENTER, maxwidth=180, color=ba.safecolor(mvp.team.color + (1, )), transition=Text.Transition.IN_LEFT, transition_delay=tval).autoretain() tval += 4 * t_incr # Most killed. most_killed = 0 mkp, mkp_name = None, None for entry in player_entries: if entry[2].killed_count >= most_killed: mkp = entry[2] mkp_name = entry[1] most_killed = entry[2].killed_count if mkp is not None: Text(ba.Lstr(resource='mostViolatedPlayerText'), color=(0.5, 0.5, 0.5, 1.0), v_align=Text.VAlign.CENTER, maxwidth=300, position=(180, ts_height / 2 - 300 + v_extra + 15), transition=Text.Transition.IN_LEFT, h_align=Text.HAlign.LEFT, transition_delay=tval).autoretain() Text(ba.Lstr(value='(${A})', subs=[('${A}', ba.Lstr(resource='deathsTallyText', subs=[('${COUNT}', str(most_killed))])) ]), position=(260, ts_height / 2 - 300 - 15 + v_extra), h_align=Text.HAlign.LEFT, scale=0.6, color=(0.3, 0.3, 0.3, 1.0), transition=Text.Transition.IN_LEFT, transition_delay=tval).autoretain() tval += 4 * t_incr Image(mkp.get_icon(), position=(233, ts_height / 2 - 300 - 30 - 46 + 25 + v_extra), scale=(50, 50), transition=Image.Transition.IN_LEFT, transition_delay=tval).autoretain() assert mkp_name is not None Text(ba.Lstr(value=mkp_name), position=(270, ts_height / 2 - 300 - 30 - 36 + v_extra + 15), h_align=Text.HAlign.LEFT, v_align=Text.VAlign.CENTER, color=ba.safecolor(mkp.team.color + (1, )), maxwidth=180, transition=Text.Transition.IN_LEFT, transition_delay=tval).autoretain() tval += 4 * t_incr # Now show individual scores. tdelay = tval Text(ba.Lstr(resource='finalScoresText'), color=(0.5, 0.5, 0.5, 1.0), position=(ts_h_offs, ts_height / 2), transition=Text.Transition.IN_RIGHT, transition_delay=tdelay).autoretain() tdelay += 4 * t_incr v_offs = 0.0 tdelay += len(player_entries) * 8 * t_incr for _score, name, prec in player_entries: tdelay -= 4 * t_incr v_offs -= 40 Text(str(prec.team.customdata['score']) if self._is_ffa else str(prec.score), color=(0.5, 0.5, 0.5, 1.0), position=(ts_h_offs + 230, ts_height / 2 + v_offs), h_align=Text.HAlign.RIGHT, transition=Text.Transition.IN_RIGHT, transition_delay=tdelay).autoretain() tdelay -= 4 * t_incr Image(prec.get_icon(), position=(ts_h_offs - 72, ts_height / 2 + v_offs + 15), scale=(30, 30), transition=Image.Transition.IN_LEFT, transition_delay=tdelay).autoretain() Text(ba.Lstr(value=name), position=(ts_h_offs - 50, ts_height / 2 + v_offs + 15), h_align=Text.HAlign.LEFT, v_align=Text.VAlign.CENTER, maxwidth=180, color=ba.safecolor(prec.team.color + (1, )), transition=Text.Transition.IN_RIGHT, transition_delay=tdelay).autoretain() ba.timer(15.0, ba.WeakCall(self._show_tips))
def show_player_scores(self, delay: float = 2.5, results: Optional[ba.TeamGameResults] = None, scale: float = 1.0, x_offset: float = 0.0, y_offset: float = 0.0) -> None: """Show scores for individual players.""" # pylint: disable=too-many-locals # pylint: disable=too-many-statements from bastd.actor.text import Text from bastd.actor.image import Image ts_v_offset = 150.0 + y_offset ts_h_offs = 80.0 + x_offset tdelay = delay spacing = 40 is_free_for_all = isinstance(self.session, ba.FreeForAllSession) def _get_prec_score(p_rec: ba.PlayerRecord) -> Optional[int]: if is_free_for_all and results is not None: assert isinstance(results, ba.TeamGameResults) val = results.get_team_score(p_rec.team) return val return p_rec.accumscore def _get_prec_score_str(p_rec: ba.PlayerRecord) -> Union[str, ba.Lstr]: if is_free_for_all and results is not None: assert isinstance(results, ba.TeamGameResults) val = results.get_team_score_str(p_rec.team) assert val is not None return val return str(p_rec.accumscore) # stats.get_records() can return players that are no longer in # the game.. if we're using results we have to filter those out # (since they're not in results and that's where we pull their # scores from) if results is not None: assert isinstance(results, ba.TeamGameResults) player_records = [] assert self.stats valid_players = list(self.stats.get_records().items()) def _get_player_score_set_entry( player: ba.Player) -> Optional[ba.PlayerRecord]: for p_rec in valid_players: # PyCharm incorrectly thinks valid_players is a List[str] # noinspection PyUnresolvedReferences if p_rec[1].player is player: return p_rec[1] return None # Results is already sorted; just convert it into a list of # score-set-entries. for winner in results.get_winners(): for team in winner.teams: if len(team.players) == 1: player_entry = _get_player_score_set_entry( team.players[0]) if player_entry is not None: player_records.append(player_entry) else: player_records = [] player_records_scores = [ (_get_prec_score(p), name, p) for name, p in list(self.stats.get_records().items()) ] player_records_scores.sort(reverse=True) # Just want living player entries. player_records = [p[2] for p in player_records_scores if p[2]] v_offs = -140.0 + spacing * len(player_records) * 0.5 def _txt(x_offs: float, y_offs: float, text: ba.Lstr, h_align: Text.HAlign = Text.HAlign.RIGHT, extrascale: float = 1.0, maxwidth: Optional[float] = 120.0) -> None: Text(text, color=(0.5, 0.5, 0.6, 0.5), position=(ts_h_offs + x_offs * scale, ts_v_offset + (v_offs + y_offs + 4.0) * scale), h_align=h_align, v_align=Text.VAlign.CENTER, scale=0.8 * scale * extrascale, maxwidth=maxwidth, transition=Text.Transition.IN_LEFT, transition_delay=tdelay).autoretain() session = self.session assert isinstance(session, ba.MultiTeamSession) tval = ba.Lstr(resource='gameLeadersText', subs=[('${COUNT}', str(session.get_game_number()))]) _txt(180, 43, tval, h_align=Text.HAlign.CENTER, extrascale=1.4, maxwidth=None) _txt(-15, 4, ba.Lstr(resource='playerText'), h_align=Text.HAlign.LEFT) _txt(180, 4, ba.Lstr(resource='killsText')) _txt(280, 4, ba.Lstr(resource='deathsText'), maxwidth=100) score_name = 'Score' if results is None else results.get_score_name() translated = ba.Lstr(translate=('scoreNames', score_name)) _txt(390, 0, translated) topkillcount = 0 topkilledcount = 99999 top_score = 0 if not player_records else _get_prec_score( player_records[0]) for prec in player_records: topkillcount = max(topkillcount, prec.accum_kill_count) topkilledcount = min(topkilledcount, prec.accum_killed_count) def _scoretxt(text: Union[str, ba.Lstr], x_offs: float, highlight: bool, delay2: float, maxwidth: float = 70.0) -> None: Text(text, position=(ts_h_offs + x_offs * scale, ts_v_offset + (v_offs + 15) * scale), scale=scale, color=(1.0, 0.9, 0.5, 1.0) if highlight else (0.5, 0.5, 0.6, 0.5), h_align=Text.HAlign.RIGHT, v_align=Text.VAlign.CENTER, maxwidth=maxwidth, transition=Text.Transition.IN_LEFT, transition_delay=tdelay + delay2).autoretain() for playerrec in player_records: tdelay += 0.05 v_offs -= spacing Image(playerrec.get_icon(), position=(ts_h_offs - 12 * scale, ts_v_offset + (v_offs + 15.0) * scale), scale=(30.0 * scale, 30.0 * scale), transition=Image.Transition.IN_LEFT, transition_delay=tdelay).autoretain() Text(ba.Lstr(value=playerrec.get_name(full=True)), maxwidth=160, scale=0.75 * scale, position=(ts_h_offs + 10.0 * scale, ts_v_offset + (v_offs + 15) * scale), h_align=Text.HAlign.LEFT, v_align=Text.VAlign.CENTER, color=ba.safecolor(playerrec.team.color + (1, )), transition=Text.Transition.IN_LEFT, transition_delay=tdelay).autoretain() _scoretxt(str(playerrec.accum_kill_count), 180, playerrec.accum_kill_count == topkillcount, 0.1) _scoretxt(str(playerrec.accum_killed_count), 280, playerrec.accum_killed_count == topkilledcount, 0.1) _scoretxt(_get_prec_score_str(playerrec), 390, _get_prec_score(playerrec) == top_score, 0.2)
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 create_display(self, x: float, y: float, delay: float, outdelay: float = None, color: Sequence[float] = None, style: str = 'post_game') -> List[ba.Actor]: """Create a display for the Achievement. Shows the Achievement icon, name, and description. """ # pylint: disable=cyclic-import from ba._lang import Lstr from ba._enums import SpecialChar from ba._coopsession import CoopSession from bastd.actor.image import Image from bastd.actor.text import Text # Yeah this needs cleaning up. if style == 'post_game': in_game_colors = False in_main_menu = False h_attach = Text.HAttach.CENTER v_attach = Text.VAttach.CENTER attach = Image.Attach.CENTER elif style == 'in_game': in_game_colors = True in_main_menu = False h_attach = Text.HAttach.LEFT v_attach = Text.VAttach.TOP attach = Image.Attach.TOP_LEFT elif style == 'news': in_game_colors = True in_main_menu = True h_attach = Text.HAttach.CENTER v_attach = Text.VAttach.TOP attach = Image.Attach.TOP_CENTER else: raise ValueError('invalid style "' + style + '"') # Attempt to determine what campaign we're in # (so we know whether to show "hard mode only"). if in_main_menu: hmo = False else: try: session = _ba.getsession() if isinstance(session, CoopSession): campaign = session.campaign assert campaign is not None hmo = (self._hard_mode_only and campaign.name == 'Easy') else: hmo = False except Exception: from ba import _error _error.print_exception('Error determining campaign') hmo = False objs: List[ba.Actor] if in_game_colors: objs = [] out_delay_fin = (delay + outdelay) if outdelay is not None else None if color is not None: cl1 = (2.0 * color[0], 2.0 * color[1], 2.0 * color[2], color[3]) cl2 = color else: cl1 = (1.5, 1.5, 2, 1.0) cl2 = (0.8, 0.8, 1.0, 1.0) if hmo: cl1 = (cl1[0], cl1[1], cl1[2], cl1[3] * 0.6) cl2 = (cl2[0], cl2[1], cl2[2], cl2[3] * 0.2) objs.append( Image(self.get_icon_texture(False), host_only=True, color=cl1, position=(x - 25, y + 5), attach=attach, transition=Image.Transition.FADE_IN, transition_delay=delay, vr_depth=4, transition_out_delay=out_delay_fin, scale=(40, 40)).autoretain()) txt = self.display_name txt_s = 0.85 txt_max_w = 300 objs.append( Text(txt, host_only=True, maxwidth=txt_max_w, position=(x, y + 2), transition=Text.Transition.FADE_IN, scale=txt_s, flatness=0.6, shadow=0.5, h_attach=h_attach, v_attach=v_attach, color=cl2, transition_delay=delay + 0.05, transition_out_delay=out_delay_fin).autoretain()) txt2_s = 0.62 txt2_max_w = 400 objs.append( Text(self.description_full if in_main_menu else self.description, host_only=True, maxwidth=txt2_max_w, position=(x, y - 14), transition=Text.Transition.FADE_IN, vr_depth=-5, h_attach=h_attach, v_attach=v_attach, scale=txt2_s, flatness=1.0, shadow=0.5, color=cl2, transition_delay=delay + 0.1, transition_out_delay=out_delay_fin).autoretain()) if hmo: txtactor = Text( Lstr(resource='difficultyHardOnlyText'), host_only=True, maxwidth=txt2_max_w * 0.7, position=(x + 60, y + 5), transition=Text.Transition.FADE_IN, vr_depth=-5, h_attach=h_attach, v_attach=v_attach, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, scale=txt_s * 0.8, flatness=1.0, shadow=0.5, color=(1, 1, 0.6, 1), transition_delay=delay + 0.1, transition_out_delay=out_delay_fin).autoretain() txtactor.node.rotate = 10 objs.append(txtactor) # Ticket-award. award_x = -100 objs.append( Text(_ba.charstr(SpecialChar.TICKET), host_only=True, position=(x + award_x + 33, y + 7), transition=Text.Transition.FADE_IN, scale=1.5, h_attach=h_attach, v_attach=v_attach, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, color=(1, 1, 1, 0.2 if hmo else 0.4), transition_delay=delay + 0.05, transition_out_delay=out_delay_fin).autoretain()) objs.append( Text('+' + str(self.get_award_ticket_value()), host_only=True, position=(x + award_x + 28, y + 16), transition=Text.Transition.FADE_IN, scale=0.7, flatness=1, h_attach=h_attach, v_attach=v_attach, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, color=cl2, transition_delay=delay + 0.05, transition_out_delay=out_delay_fin).autoretain()) else: complete = self.complete objs = [] c_icon = self.get_icon_color(complete) if hmo and not complete: c_icon = (c_icon[0], c_icon[1], c_icon[2], c_icon[3] * 0.3) objs.append( Image(self.get_icon_texture(complete), host_only=True, color=c_icon, position=(x - 25, y + 5), attach=attach, vr_depth=4, transition=Image.Transition.IN_RIGHT, transition_delay=delay, transition_out_delay=None, scale=(40, 40)).autoretain()) if complete: objs.append( Image(_ba.gettexture('achievementOutline'), host_only=True, model_transparent=_ba.getmodel('achievementOutline'), color=(2, 1.4, 0.4, 1), vr_depth=8, position=(x - 25, y + 5), attach=attach, transition=Image.Transition.IN_RIGHT, transition_delay=delay, transition_out_delay=None, scale=(40, 40)).autoretain()) else: if not complete: award_x = -100 objs.append( Text(_ba.charstr(SpecialChar.TICKET), host_only=True, position=(x + award_x + 33, y + 7), transition=Text.Transition.IN_RIGHT, scale=1.5, h_attach=h_attach, v_attach=v_attach, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, color=(1, 1, 1, 0.4) if complete else (1, 1, 1, (0.1 if hmo else 0.2)), transition_delay=delay + 0.05, transition_out_delay=None).autoretain()) objs.append( Text('+' + str(self.get_award_ticket_value()), host_only=True, position=(x + award_x + 28, y + 16), transition=Text.Transition.IN_RIGHT, scale=0.7, flatness=1, h_attach=h_attach, v_attach=v_attach, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, color=((0.8, 0.93, 0.8, 1.0) if complete else (0.6, 0.6, 0.6, (0.2 if hmo else 0.4))), transition_delay=delay + 0.05, transition_out_delay=None).autoretain()) # Show 'hard-mode-only' only over incomplete achievements # when that's the case. if hmo: txtactor = Text( Lstr(resource='difficultyHardOnlyText'), host_only=True, maxwidth=300 * 0.7, position=(x + 60, y + 5), transition=Text.Transition.FADE_IN, vr_depth=-5, h_attach=h_attach, v_attach=v_attach, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, scale=0.85 * 0.8, flatness=1.0, shadow=0.5, color=(1, 1, 0.6, 1), transition_delay=delay + 0.05, transition_out_delay=None).autoretain() assert txtactor.node txtactor.node.rotate = 10 objs.append(txtactor) objs.append( Text(self.display_name, host_only=True, maxwidth=300, position=(x, y + 2), transition=Text.Transition.IN_RIGHT, scale=0.85, flatness=0.6, h_attach=h_attach, v_attach=v_attach, color=((0.8, 0.93, 0.8, 1.0) if complete else (0.6, 0.6, 0.6, (0.2 if hmo else 0.4))), transition_delay=delay + 0.05, transition_out_delay=None).autoretain()) objs.append( Text(self.description_complete if complete else self.description, host_only=True, maxwidth=400, position=(x, y - 14), transition=Text.Transition.IN_RIGHT, vr_depth=-5, h_attach=h_attach, v_attach=v_attach, scale=0.62, flatness=1.0, color=((0.6, 0.6, 0.6, 1.0) if complete else (0.6, 0.6, 0.6, (0.2 if hmo else 0.4))), transition_delay=delay + 0.1, transition_out_delay=None).autoretain()) return objs
def show_player_scores(self, delay: float = 2.5, results: Optional[ba.GameResults] = None, scale: float = 1.0, x_offset: float = 0.0, y_offset: float = 0.0) -> None: """Show scores for individual players.""" # pylint: disable=too-many-locals # pylint: disable=too-many-statements ts_v_offset = 150.0 + y_offset ts_h_offs = 80.0 + x_offset tdelay = delay spacing = 40 is_free_for_all = isinstance(self.session, ba.FreeForAllSession) is_two_team = True if len(self.session.sessionteams) == 2 else False def _get_prec_score(p_rec: ba.PlayerRecord) -> Optional[int]: if is_free_for_all and results is not None: assert isinstance(results, ba.GameResults) assert p_rec.team.activityteam is not None val = results.get_sessionteam_score(p_rec.team) return val return p_rec.accumscore def _get_prec_score_str(p_rec: ba.PlayerRecord) -> Union[str, ba.Lstr]: if is_free_for_all and results is not None: assert isinstance(results, ba.GameResults) assert p_rec.team.activityteam is not None val = results.get_sessionteam_score_str(p_rec.team) assert val is not None return val return str(p_rec.accumscore) # stats.get_records() can return players that are no longer in # the game.. if we're using results we have to filter those out # (since they're not in results and that's where we pull their # scores from) if results is not None: assert isinstance(results, ba.GameResults) player_records = [] assert self.stats valid_players = list(self.stats.get_records().items()) def _get_player_score_set_entry( player: ba.SessionPlayer) -> Optional[ba.PlayerRecord]: for p_rec in valid_players: if p_rec[1].player is player: return p_rec[1] return None # Results is already sorted; just convert it into a list of # score-set-entries. for winnergroup in results.winnergroups: for team in winnergroup.teams: if len(team.players) == 1: player_entry = _get_player_score_set_entry(team.players[0]) if player_entry is not None: player_records.append(player_entry) else: player_records = [] player_records_scores = [ (_get_prec_score(p), name, p) for name, p in list(self.stats.get_records().items()) ] player_records_scores.sort(reverse=True) # Just want living player entries. player_records = [p[2] for p in player_records_scores if p[2]] voffs = -140.0 + spacing * 5 * 0.5 voffs_team0 = voffs tdelay_team0 = tdelay def _txt(xoffs: float, yoffs: float, text: ba.Lstr, h_align: Text.HAlign = Text.HAlign.RIGHT, extrascale: float = 1.0, maxwidth: Optional[float] = 120.0) -> None: Text(text, color=(0.5, 0.5, 0.6, 0.5), position=(ts_h_offs + xoffs * scale, ts_v_offset + (voffs + yoffs + 4.0) * scale), h_align=h_align, v_align=Text.VAlign.CENTER, scale=0.8 * scale * extrascale, maxwidth=maxwidth, transition=Text.Transition.IN_LEFT, transition_delay=tdelay).autoretain() session = self.session assert isinstance(session, ba.MultiTeamSession) if is_two_team: tval = "Game " + str(session.get_game_number()) + " Results" _txt(-75, 160, tval, h_align=Text.HAlign.CENTER, extrascale=1.4, maxwidth=None) _txt(-15, 4, ba.Lstr(resource='playerText'), h_align=Text.HAlign.LEFT) _txt(180, 4, ba.Lstr(resource='killsText')) _txt(280, 4, ba.Lstr(resource='deathsText'), maxwidth=100) score_label = 'Score' if results is None else results.score_label translated = ba.Lstr(translate=('scoreNames', score_label)) _txt(390, 0, translated) if is_two_team: _txt(-595, 4, ba.Lstr(resource='playerText'), h_align=Text.HAlign.LEFT) _txt(-400, 4, ba.Lstr(resource='killsText')) _txt(-300, 4, ba.Lstr(resource='deathsText'), maxwidth=100) _txt(-190, 0, translated) topkillcount = 0 topkilledcount = 99999 top_score = 0 if not player_records else _get_prec_score(player_records[0]) for prec in player_records: topkillcount = max(topkillcount, prec.accum_kill_count) topkilledcount = min(topkilledcount, prec.accum_killed_count) def _scoretxt(text: Union[str, ba.Lstr], x_offs: float, highlight: bool, delay2: float, maxwidth: float = 70.0, team_id=1) -> None: Text(text, position=(ts_h_offs + x_offs * scale, ts_v_offset + (voffs + 15) * scale) if team_id == 1 else (ts_h_offs + x_offs * scale, ts_v_offset + (voffs_team0 + 15) * scale), scale=scale, color=(1.0, 0.9, 0.5, 1.0) if highlight else (0.5, 0.5, 0.6, 0.5), h_align=Text.HAlign.RIGHT, v_align=Text.VAlign.CENTER, maxwidth=maxwidth, transition=Text.Transition.IN_LEFT, transition_delay=(tdelay + delay2) if team_id == 1 else (tdelay_team0 + delay2)).autoretain() for playerrec in player_records: if is_two_team and playerrec.team.id == 0: tdelay_team0 += 0.05 voffs_team0 -= spacing x_image = 617 x_text = -595 y = ts_v_offset + (voffs_team0 + 15.0) * scale else: tdelay += 0.05 voffs -= spacing x_image = 12 x_text = 10.0 y = ts_v_offset + (voffs + 15.0) * scale Image(playerrec.get_icon(), position=(ts_h_offs - x_image * scale, y), scale=(30.0 * scale, 30.0 * scale), transition=Image.Transition.IN_LEFT, transition_delay=tdelay if playerrec.team.id == 1 else tdelay_team0).autoretain() Text(ba.Lstr(value=playerrec.getname(full=True)), maxwidth=160, scale=0.75 * scale, position=(ts_h_offs + x_text * scale, y), h_align=Text.HAlign.LEFT, v_align=Text.VAlign.CENTER, color=ba.safecolor(playerrec.team.color + (1, )), transition=Text.Transition.IN_LEFT, transition_delay=tdelay if playerrec.team.id == 1 else tdelay_team0).autoretain() if is_two_team and playerrec.team.id == 0: _scoretxt(str(playerrec.accum_kill_count), -400, playerrec.accum_kill_count == topkillcount, 0.1, team_id=0) _scoretxt(str(playerrec.accum_killed_count), -300, playerrec.accum_killed_count == topkilledcount, 0.1, team_id=0) _scoretxt(_get_prec_score_str(playerrec), -190, _get_prec_score(playerrec) == top_score, 0.2, team_id=0) else: _scoretxt(str(playerrec.accum_kill_count), 180, playerrec.accum_kill_count == topkillcount, 0.1) _scoretxt(str(playerrec.accum_killed_count), 280, playerrec.accum_killed_count == topkilledcount, 0.1) _scoretxt(_get_prec_score_str(playerrec), 390, _get_prec_score(playerrec) == top_score, 0.2)
def on_begin(self) -> None: # pylint: disable=too-many-locals # pylint: disable=too-many-statements from bastd.actor.text import Text from bastd.actor.image import Image ba.set_analytics_screen('FreeForAll Score Screen') super().on_begin() y_base = 100.0 ts_h_offs = -305.0 tdelay = 1.0 scale = 1.2 spacing = 37.0 # We include name and previous score in the sort to reduce the amount # of random jumping around the list we do in cases of ties. player_order_prev = list(self.players) player_order_prev.sort( reverse=True, key=lambda p: ( p.team.sessionteam.customdata['previous_score'], p.getname(full=True), )) player_order = list(self.players) player_order.sort(reverse=True, key=lambda p: ( p.team.sessionteam.customdata['score'], p.team.sessionteam.customdata['score'], p.getname(full=True), )) v_offs = -74.0 + spacing * len(player_order_prev) * 0.5 delay1 = 1.3 + 0.1 delay2 = 2.9 + 0.1 delay3 = 2.9 + 0.1 order_change = player_order != player_order_prev if order_change: delay3 += 1.5 ba.timer(0.3, ba.Call(ba.playsound, self._score_display_sound)) results = self.settings_raw['results'] assert isinstance(results, ba.GameResults) self.show_player_scores(delay=0.001, results=results, scale=1.2, x_offset=-110.0) sound_times: Set[float] = set() def _scoretxt(text: str, x_offs: float, y_offs: float, highlight: bool, delay: float, extrascale: float, flash: bool = False) -> Text: return Text(text, position=(ts_h_offs + x_offs * scale, y_base + (y_offs + v_offs + 2.0) * scale), scale=scale * extrascale, color=((1.0, 0.7, 0.3, 1.0) if highlight else (0.7, 0.7, 0.7, 0.7)), h_align=Text.HAlign.RIGHT, transition=Text.Transition.IN_LEFT, transition_delay=tdelay + delay, flash=flash).autoretain() v_offs -= spacing slide_amt = 0.0 transtime = 0.250 transtime2 = 0.250 session = self.session assert isinstance(session, ba.FreeForAllSession) title = Text(ba.Lstr(resource='firstToSeriesText', subs=[('${COUNT}', str(session.get_ffa_series_length()))]), scale=1.05 * scale, position=(ts_h_offs - 0.0 * scale, y_base + (v_offs + 50.0) * scale), h_align=Text.HAlign.CENTER, color=(0.5, 0.5, 0.5, 0.5), transition=Text.Transition.IN_LEFT, transition_delay=tdelay).autoretain() v_offs -= 25 v_offs_start = v_offs ba.timer( tdelay + delay3, ba.WeakCall( self._safe_animate, title.position_combine, 'input0', { 0.0: ts_h_offs - 0.0 * scale, transtime2: ts_h_offs - (0.0 + slide_amt) * scale })) for i, player in enumerate(player_order_prev): v_offs_2 = v_offs_start - spacing * (player_order.index(player)) ba.timer(tdelay + 0.3, ba.Call(ba.playsound, self._score_display_sound_small)) if order_change: ba.timer(tdelay + delay2 + 0.1, ba.Call(ba.playsound, self._cymbal_sound)) img = Image(player.get_icon(), position=(ts_h_offs - 72.0 * scale, y_base + (v_offs + 15.0) * scale), scale=(30.0 * scale, 30.0 * scale), transition=Image.Transition.IN_LEFT, transition_delay=tdelay).autoretain() ba.timer( tdelay + delay2, ba.WeakCall( self._safe_animate, img.position_combine, 'input1', { 0: y_base + (v_offs + 15.0) * scale, transtime: y_base + (v_offs_2 + 15.0) * scale })) ba.timer( tdelay + delay3, ba.WeakCall( self._safe_animate, img.position_combine, 'input0', { 0: ts_h_offs - 72.0 * scale, transtime2: ts_h_offs - (72.0 + slide_amt) * scale })) txt = Text(ba.Lstr(value=player.getname(full=True)), maxwidth=130.0, scale=0.75 * scale, position=(ts_h_offs - 50.0 * scale, y_base + (v_offs + 15.0) * scale), h_align=Text.HAlign.LEFT, v_align=Text.VAlign.CENTER, color=ba.safecolor(player.team.color + (1, )), transition=Text.Transition.IN_LEFT, transition_delay=tdelay).autoretain() ba.timer( tdelay + delay2, ba.WeakCall( self._safe_animate, txt.position_combine, 'input1', { 0: y_base + (v_offs + 15.0) * scale, transtime: y_base + (v_offs_2 + 15.0) * scale })) ba.timer( tdelay + delay3, ba.WeakCall( self._safe_animate, txt.position_combine, 'input0', { 0: ts_h_offs - 50.0 * scale, transtime2: ts_h_offs - (50.0 + slide_amt) * scale })) txt_num = Text('#' + str(i + 1), scale=0.55 * scale, position=(ts_h_offs - 95.0 * scale, y_base + (v_offs + 8.0) * scale), h_align=Text.HAlign.RIGHT, color=(0.6, 0.6, 0.6, 0.6), transition=Text.Transition.IN_LEFT, transition_delay=tdelay).autoretain() ba.timer( tdelay + delay3, ba.WeakCall( self._safe_animate, txt_num.position_combine, 'input0', { 0: ts_h_offs - 95.0 * scale, transtime2: ts_h_offs - (95.0 + slide_amt) * scale })) s_txt = _scoretxt( str(player.team.sessionteam.customdata['previous_score']), 80, 0, False, 0, 1.0) ba.timer( tdelay + delay2, ba.WeakCall( self._safe_animate, s_txt.position_combine, 'input1', { 0: y_base + (v_offs + 2.0) * scale, transtime: y_base + (v_offs_2 + 2.0) * scale })) ba.timer( tdelay + delay3, ba.WeakCall( self._safe_animate, s_txt.position_combine, 'input0', { 0: ts_h_offs + 80.0 * scale, transtime2: ts_h_offs + (80.0 - slide_amt) * scale })) score_change = ( player.team.sessionteam.customdata['score'] - player.team.sessionteam.customdata['previous_score']) if score_change > 0: xval = 113 yval = 3.0 s_txt_2 = _scoretxt('+' + str(score_change), xval, yval, True, 0, 0.7, flash=True) ba.timer( tdelay + delay2, ba.WeakCall( self._safe_animate, s_txt_2.position_combine, 'input1', { 0: y_base + (v_offs + yval + 2.0) * scale, transtime: y_base + (v_offs_2 + yval + 2.0) * scale })) ba.timer( tdelay + delay3, ba.WeakCall( self._safe_animate, s_txt_2.position_combine, 'input0', { 0: ts_h_offs + xval * scale, transtime2: ts_h_offs + (xval - slide_amt) * scale })) def _safesetattr(node: Optional[ba.Node], attr: str, value: Any) -> None: if node: setattr(node, attr, value) ba.timer( tdelay + delay1, ba.Call(_safesetattr, s_txt.node, 'color', (1, 1, 1, 1))) for j in range(score_change): ba.timer((tdelay + delay1 + 0.15 * j), ba.Call( _safesetattr, s_txt.node, 'text', str(player.team.sessionteam. customdata['previous_score'] + j + 1))) tfin = tdelay + delay1 + 0.15 * j if tfin not in sound_times: sound_times.add(tfin) ba.timer( tfin, ba.Call(ba.playsound, self._score_display_sound_small)) v_offs -= spacing